Compare commits
14 Commits
8f8fa8f8d1
...
ac2
| Author | SHA1 | Date | |
|---|---|---|---|
| b639bc6c4f | |||
| 7ee04c78cd | |||
| 7b58addf7e | |||
| ae52be591b | |||
| 93ab7690ba | |||
| 68c3f88181 | |||
| 830864aa8a | |||
| 5f34cb34b2 | |||
| b9d2fe60fd | |||
| 06bc4fcd55 | |||
| 42cb84b610 | |||
| f8607d114c | |||
| 56230dd340 | |||
| df9fdf26c8 |
@@ -1,549 +0,0 @@
|
|||||||
# Dusk — Claude Code rules
|
|
||||||
|
|
||||||
See `STATUS.md` for a periodically-refreshed inventory of subsystem
|
|
||||||
maturity, test coverage gaps, and known open issues — check it before
|
|
||||||
assuming a subsystem is fully wired up or before picking a next task.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
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
|
|
||||||
|
|
||||||
### 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 |
|
|
||||||
| `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)
|
|
||||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP)
|
|
||||||
src/dusklinux/ Linux + Knulli platform impl
|
|
||||||
src/duskpsp/ PSP 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 asset loader type
|
|
||||||
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
|
|
||||||
`src/dusk/asset/loader/assetloader.h`.
|
|
||||||
2. Add fields to the input/loading/output unions in `assetloader.h`.
|
|
||||||
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
|
|
||||||
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
|
|
||||||
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
|
|
||||||
`src/dusk/asset/loader/assetloader.c`.
|
|
||||||
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new entity component
|
|
||||||
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
|
||||||
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
|
||||||
`entityMyCompDispose()`, `entityMyCompRender()`.
|
|
||||||
2. Add the include to `src/dusk/entity/componentlist.h` header block
|
|
||||||
(or `src/duskrpg/entity/gamecomponentlist.h` for a game-specific
|
|
||||||
component, appended after the engine's inbuilt ones).
|
|
||||||
3. Add a row:
|
|
||||||
```c
|
|
||||||
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
|
||||||
```
|
|
||||||
Params are `(enumName, type, field, init, dispose, render)` — pass
|
|
||||||
`NULL` for any callback the component doesn't need. This
|
|
||||||
auto-generates the enum, union field, and definition entry.
|
|
||||||
4. If JS-facing, create the script module and `.d.ts` (see below).
|
|
||||||
|
|
||||||
Entities/components/scenes have no JSON serialize/deserialize path —
|
|
||||||
that was removed in favor of building scenes from C-coded prefabs
|
|
||||||
(below) or from JerryScript (`Entity`/`Component`/`Scene`, see
|
|
||||||
"Adding a new script (JS) module").
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new entity/scene prefab
|
|
||||||
Entity prefabs (`src/dusk/entity/entityprefab.h`) and scene prefabs
|
|
||||||
(`src/dusk/scene/sceneprefab.h`) follow the same pattern:
|
|
||||||
1. Write an apply function: `errorret_t entityPrefabXxxApply(mgr,
|
|
||||||
entityId)` (or `errorret_t scenePrefabXxxApply(sceneId)`), building
|
|
||||||
up the entity/scene with the normal component/entity APIs.
|
|
||||||
2. Add an entry to the sentinel-terminated `ENTITY_PREFABS[]` (in
|
|
||||||
`src/dusk/entity/entityprefablist.h`, or a game-specific list it
|
|
||||||
includes) or `SCENE_PREFABS[]`:
|
|
||||||
```c
|
|
||||||
{ .name = "MY_PREFAB", .extends = "", .apply = entityPrefabXxxApply }
|
|
||||||
```
|
|
||||||
`extends` names another prefab to apply first (recurses through
|
|
||||||
`entityPrefabResolveAndApply`/`scenePrefabResolveAndApply`), or `""`
|
|
||||||
for none. Do not add an enum or count field — the array is iterated
|
|
||||||
until `.name[0] == '\0'`.
|
|
||||||
3. `entityPrefabResolveAndApply`/`scenePrefabResolveAndApply` only
|
|
||||||
resolve names against the C-coded registry above — there is no JSON
|
|
||||||
asset fallback. Throws if no prefab with that name is registered.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new cutscene item type
|
|
||||||
1. Create `src/duskrpg/cutscene/item/<category>/cutsceneMyItem.h/.c`
|
|
||||||
with a data struct (e.g. `cutscenemyitem_t`) and
|
|
||||||
`cutsceneMyItemStart(item, data)` / `cutsceneMyItemUpdate(item,
|
|
||||||
data)` (the latter returns `true` once the item has completed). Add
|
|
||||||
a matching `cutscenemyitemdata_t` runtime-data struct only if the
|
|
||||||
item needs per-run state across ticks (most don't).
|
|
||||||
2. Add `CUTSCENE_ITEM_TYPE_MY_ITEM` to the enum and a union member to
|
|
||||||
`cutsceneitem_t` (and `cutsceneitemdata_t` if it has runtime data) in
|
|
||||||
`src/duskrpg/cutscene/item/cutsceneitem.h`.
|
|
||||||
3. Register the `{ start, update }` pair in `CUTSCENE_ITEM_CALLBACKS[]`
|
|
||||||
in `cutsceneitem.c`.
|
|
||||||
4. Add an authoring macro to `src/duskrpg/cutscene/cutscene.h`:
|
|
||||||
```c
|
|
||||||
#define CUTSCENE_MY_ITEM(ARGS...) \
|
|
||||||
{ .type = CUTSCENE_ITEM_TYPE_MY_ITEM, .myItem = { ARGS } }
|
|
||||||
```
|
|
||||||
used inside a `CUTSCENE(NAME, SIZE, PAUSE_TYPE, ...)` block.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Save system
|
|
||||||
Save data lives under `src/dusk/save/` (`save.h`/`savefile.h`/
|
|
||||||
`saveplatform.h`). Slots are fixed-count (`SAVE_FILE_COUNT_MAX`), each
|
|
||||||
holding a yyjson document persisted with its byte size and a CRC32
|
|
||||||
checksum. `saveLoad`/`saveSave` read/write the JSON fresh every call —
|
|
||||||
callers own the returned/passed `yyjson_doc`/`yyjson_mut_doc` and must
|
|
||||||
free it themselves. Actual file I/O goes through platform-specific
|
|
||||||
`saveplatform_t`/stream hooks (one implementation per platform under
|
|
||||||
`src/dusk<platform>/save/`) — do not add direct filesystem calls to the
|
|
||||||
core `save.c`, extend the platform stream hooks instead.
|
|
||||||
|
|
||||||
## Network system
|
|
||||||
`src/dusk/network/` (`network.h`) is a connection-state layer only — a
|
|
||||||
`networkstate_t` state machine (`DISCONNECTED`/`CONNECTING`/`CONNECTED`/
|
|
||||||
`DISCONNECTING`) plus an HTTP client (`network/http/`) used for one-off
|
|
||||||
requests. It is not a multiplayer/replication protocol — that layer
|
|
||||||
doesn't exist yet (see `ROADMAP.md` items on the socket server/client
|
|
||||||
and packet handlers). Platform-specific connection logic (e.g. PSP's
|
|
||||||
`sceNetApctl` polling, GameCube/Wii's `if_config()`) lives under
|
|
||||||
`src/dusk<platform>/network/`, wired through `networkplatform.h` macros
|
|
||||||
the same way display/input are. Whenever this eventually grows a
|
|
||||||
multiplayer protocol, apply `ROADMAP.md`'s principle: never trust
|
|
||||||
incoming packet data — validate defensively with `errorret_t`/
|
|
||||||
`errorThrow()`, not asserts.
|
|
||||||
|
|
||||||
## Adding a new script (JS) module
|
|
||||||
Dusk embeds JerryScript (`src/dusk/script/`, fetched via
|
|
||||||
`cmake/modules/Findjerryscript.cmake`). Today only `Entity`, the
|
|
||||||
generic `Component` wrapper, and `Scene` are registered (see
|
|
||||||
`src/dusk/script/module/modulelist.c`) — no per-component-type typed
|
|
||||||
wrappers exist yet (e.g. no `.position` on a `POSITION` component);
|
|
||||||
`entity.add(TYPE)`/`entity.getComponent(TYPE)` always return the
|
|
||||||
generic `Component`.
|
|
||||||
|
|
||||||
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 —
|
|
||||||
these are the one exception to "no `static` in `.c` files": the
|
|
||||||
macro itself expands to a `static jerry_value_t name(...)`
|
|
||||||
JerryScript external-handler trampoline, never called by name
|
|
||||||
from other C files, so it isn't declared in the `.h`.
|
|
||||||
- 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 a component module that adds a *typed* wrapper for a specific
|
|
||||||
component type, create
|
|
||||||
`src/dusk/script/module/entity/component/modulecomponentlist.c` (it
|
|
||||||
doesn't exist yet — the first such module creates it) so
|
|
||||||
`entity.add()` can return the typed wrapper instead of the generic
|
|
||||||
`Component`.
|
|
||||||
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+0000–U+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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Color system
|
|
||||||
|
|
||||||
Colors are defined in `src/dusk/display/color.csv` and code-generated
|
|
||||||
into a `color.h` header by `tools/color/csv/__main__.py`.
|
|
||||||
|
|
||||||
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
|
|
||||||
The script emits four `#define` variants per color plus a bare alias:
|
|
||||||
|
|
||||||
```
|
|
||||||
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
|
|
||||||
COLOR_<NAME>_3B color3b(r8, g8, b8)
|
|
||||||
COLOR_<NAME>_3F color3f(rf, gf, bf)
|
|
||||||
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
|
|
||||||
COLOR_<NAME> COLOR_<NAME>_4B
|
|
||||||
```
|
|
||||||
|
|
||||||
`color_t` is `color4b_t` (four `uint8_t` channels).
|
|
||||||
|
|
||||||
To add a new color, append a row to `color.csv` and rebuild — do not
|
|
||||||
hand-edit the generated header.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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`.
|
|
||||||
+12
-1
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
|
|||||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||||
|
|
||||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||||
|
option(DUSK_NETWORKING "Enable networking support" OFF)
|
||||||
|
|
||||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||||
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
DUSK_VERSION="${DUSK_VERSION}"
|
DUSK_VERSION="${DUSK_VERSION}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(DUSK_NETWORKING)
|
||||||
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
|
DUSK_NETWORKING
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Toolchains
|
# Toolchains
|
||||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||||
|
|
||||||
@@ -116,7 +123,11 @@ if(DUSK_BUILD_TESTS)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Build assets
|
# Build assets
|
||||||
file(GLOB_RECURSE DUSK_ASSET_FILES CONFIGURE_DEPENDS "${DUSK_ASSETS_DIR}/*")
|
# Deliberately not CONFIGURE_DEPENDS: that reruns the full CMake configure
|
||||||
|
# step (which invalidates generated headers and forces a huge rebuild) on
|
||||||
|
# every single asset edit. Re-run cmake manually when assets are added or
|
||||||
|
# removed.
|
||||||
|
file(GLOB_RECURSE DUSK_ASSET_FILES "${DUSK_ASSETS_DIR}/*")
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT "${DUSK_ASSETS_ZIP}"
|
OUTPUT "${DUSK_ASSETS_ZIP}"
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
|
||||||
|
|||||||
@@ -1,260 +0,0 @@
|
|||||||
# PSP Optimization Plan
|
|
||||||
|
|
||||||
A survey of concrete memory and CPU (especially floating-point) optimization
|
|
||||||
opportunities for the PSP target, done 2026-07-31 on branch `ac2` at commit
|
|
||||||
`e61914ba` + this session's scripting work. Point-in-time findings, not a
|
|
||||||
substitute for reading the referenced source before acting on it.
|
|
||||||
|
|
||||||
## Why this exists
|
|
||||||
|
|
||||||
The PSP (Allegrex MIPS CPU, 222-333MHz, single core, 32-64MB main RAM, 2MB
|
|
||||||
eDRAM, 16KB I-cache / 16KB D-cache, no virtual memory or memory protection)
|
|
||||||
is the tightest-constrained target this engine ships to. Two rules guide
|
|
||||||
everything below, both already correctly applied in one place in this
|
|
||||||
codebase (`entityposition_t` caching its transform matrices instead of
|
|
||||||
recomputing them from position/rotation/scale every read — see "Already
|
|
||||||
correct" below):
|
|
||||||
|
|
||||||
- **Memory is finite and cannot be compacted.** No VM means a fragmented
|
|
||||||
heap after a few minutes of play can fail an allocation even with
|
|
||||||
"enough" total free bytes. Prefer static/pool allocation over
|
|
||||||
malloc/free churn; prefer trading memory for CPU only when the memory
|
|
||||||
cost is bounded and paid once.
|
|
||||||
- **CPU is finite and floating-point math is not free**, especially
|
|
||||||
trig/sqrt on the plain scalar FPU. Prefer caching a computed result
|
|
||||||
behind a dirty flag over recomputing it unconditionally; prefer
|
|
||||||
avoiding a sqrt via squared-distance comparison wherever only a
|
|
||||||
yes/no or ordering result is needed.
|
|
||||||
|
|
||||||
## Priority 1 — Entity/component memory: a union tax paid on every slot, times 4 scenes
|
|
||||||
|
|
||||||
**The single biggest finding.** `entitymanager_t` (`src/dusk/entity/entitymanager.h:11-15`)
|
|
||||||
is a flat, statically-sized struct:
|
|
||||||
|
|
||||||
```c
|
|
||||||
typedef struct entitymanager_t {
|
|
||||||
entity_t entities[ENTITY_COUNT_MAX]; // 64
|
|
||||||
component_t components[ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX]; // 64*16 = 1024
|
|
||||||
componentid_t entitiesWithComponent[COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX];
|
|
||||||
} entitymanager_t;
|
|
||||||
```
|
|
||||||
|
|
||||||
`component_t` (`src/dusk/entity/component.h:69-72`) holds a **tagged union**
|
|
||||||
(`componentdata_t`) sized to its largest variant. That variant is
|
|
||||||
`entityrenderable_t`'s spritebatch payload (`entityrenderable.h:25-66`):
|
|
||||||
`spritebatchsprite_t sprites[64]` at 40 bytes each (`vec3 min + vec3 max +
|
|
||||||
vec2 uvMin + vec2 uvMax`) = **2560 bytes**, versus ~28-300 bytes for every
|
|
||||||
other component type (camera, physics, animation, position, trigger).
|
|
||||||
|
|
||||||
Because the union is embedded by value in a flat array — not behind a
|
|
||||||
pointer, not sparse — **every one of the 1024 component slots in every
|
|
||||||
entity manager costs ~2580 bytes, whether it holds a 28-byte camera or
|
|
||||||
nothing at all.** `1024 * 2580 ≈ 2.52MB` per `entitymanager_t`, confirmed
|
|
||||||
by the engine's own startup log every run: `Entity manager size: 2684992
|
|
||||||
bytes (2622.06 KB)`.
|
|
||||||
|
|
||||||
`scene_t` (`src/dusk/scene/scene.h:14-18`) embeds `entitymanager_t` by
|
|
||||||
value too, and `SCENE_COUNT_MAX = 4` (`scene/scenebase.h:11`) with
|
|
||||||
`SCENE_MANAGER` declared as a plain global (`scene/scene.c:20`) — so this
|
|
||||||
~2.52MB exists as static BSS from process start for **all 4 scene slots**,
|
|
||||||
used or not. **Total: ~10.1MB reserved permanently for entity storage
|
|
||||||
alone** — 16-31% of PSP main RAM — before a single texture, mesh, or audio
|
|
||||||
asset is loaded.
|
|
||||||
|
|
||||||
**Options to fix (roughly increasing effort/risk):**
|
|
||||||
1. Pull `entityrenderablespritebatch_t` out of the component union entirely
|
|
||||||
— store spritebatch entities via a pointer/handle into a separate,
|
|
||||||
smaller fixed pool sized to how many spritebatch-renderable entities
|
|
||||||
actually coexist (almost certainly far fewer than 64 per entity, and
|
|
||||||
far fewer entities need spritebatch at all vs. mesh/material
|
|
||||||
rendering). This alone would shrink the union's dominant variant from
|
|
||||||
~2560 bytes to whatever the next-largest variant is (~300 bytes,
|
|
||||||
`entitytrigger_t`) — an ~88% reduction, dropping the ~10.1MB down to
|
|
||||||
roughly ~1.2MB.
|
|
||||||
2. Reduce `SCENE_COUNT_MAX` from 4 if 4 concurrent scenes were never a
|
|
||||||
deliberate requirement (check with the project owner — this may just
|
|
||||||
be a round-number default nobody revisited), or make scene storage
|
|
||||||
pointer-based/lazily allocated so unused scene slots cost ~0 instead
|
|
||||||
of a full `entitymanager_t`.
|
|
||||||
3. Reduce `ENTITY_COMPONENT_COUNT_MAX` (16) if entities realistically use
|
|
||||||
far fewer distinct component types simultaneously — check actual
|
|
||||||
usage across `entityprefablist.h`/`gameprefablist.h` prefabs.
|
|
||||||
|
|
||||||
Do (1) first — it's the highest-leverage, most contained change (touches
|
|
||||||
`entityrenderable.h`'s data layout and its render/dispose paths, not the
|
|
||||||
general entity/component system), and re-measure via the same startup
|
|
||||||
log line before deciding whether (2)/(3) are still worth doing.
|
|
||||||
|
|
||||||
## Priority 2 — VFPU is completely unused; all vec/mat math is scalar
|
|
||||||
|
|
||||||
The PSP's Allegrex CPU has a **VFPU** (vector floating-point unit) capable
|
|
||||||
of fast SIMD-style 4-wide float ops and hardware-accelerated matrix
|
|
||||||
operations, exposed by pspsdk's `pspvfpu`/GU "Geometry Utility" (`gum_*`)
|
|
||||||
helpers. This engine links `pspvfpu` (`cmake/targets/psp.cmake`) but
|
|
||||||
**never calls into it** — confirmed via a zero-hit grep for
|
|
||||||
`vfpu|pspmath|gum_|vcst|gu_matrix` across `src/duskpsp/` and `src/duskgl/`.
|
|
||||||
|
|
||||||
All vector/matrix math instead goes through cglm (`glm_vec3_*`,
|
|
||||||
`glm_mat4_*`), which auto-detects SIMD only for x86 (`CGLM_SSE2`/`AVX`) or
|
|
||||||
ARM (`CGLM_NEON`) — neither applies to MIPS, and no `CGLM_*` macros are
|
|
||||||
set anywhere in this repo (confirmed zero hits). **Every `glm_mat4_mul`,
|
|
||||||
every position/rotation rebuild, every physics vector op runs on the
|
|
||||||
plain scalar MIPS FPU with the VFPU sitting idle.**
|
|
||||||
|
|
||||||
This is the single largest *available* CPU win identified in this survey
|
|
||||||
— larger than any specific hot-path fix below, because it's a multiplier
|
|
||||||
on all of them. Concretely: route `entityposition.c`'s matrix
|
|
||||||
rebuild/multiply path and `physicsworld.c`'s per-body vector math through
|
|
||||||
`pspvfpu`/`gum_*` on the PSP build specifically (behind `#ifdef DUSK_PSP`,
|
|
||||||
matching the project's existing platform-guard convention), while keeping
|
|
||||||
cglm as the portable fallback for Linux/Knulli/GameCube/Wii. This is a
|
|
||||||
genuinely large effort (new platform-specific math backend, careful
|
|
||||||
correctness verification since VFPU has its own quirks around
|
|
||||||
pipelining/hazards) — scope it as its own project, not a quick pass.
|
|
||||||
|
|
||||||
## Priority 3 — UI/spritebatch rebuilds and redraws every vertex, every frame
|
|
||||||
|
|
||||||
Already flagged in `STATUS.md`/`ROADMAP.md` (item 4/5) as a known open
|
|
||||||
issue; this session's research adds concrete numbers. `meshvertex_t`
|
|
||||||
(`display/mesh/meshvertex.h:14-17`) is `{ float uv[2]; float pos[3]; }` =
|
|
||||||
**20 bytes/vertex**, no packing. `SPRITEBATCH_SPRITES_MAX = 512`,
|
|
||||||
`SPRITEBATCH_FLUSH_COUNT = 16` → 32 sprites/flush = 192 vertices = **3,840
|
|
||||||
bytes rebuilt and redrawn per flush**, with a flush forced on every
|
|
||||||
shader/material change (`spritebatch.c:38-50`) and used unconditionally
|
|
||||||
by every widget except `uiconsole.c` (`uitab.c:56`, `uislider.c:183/199/231`,
|
|
||||||
final flush in `ui.c:51`). A UI screen with ~50-100 sprites and 2-3
|
|
||||||
material changes costs an estimated **8-15KB of vertex rebuild + GU
|
|
||||||
submission every single frame**, regardless of whether the UI changed
|
|
||||||
since the last frame.
|
|
||||||
|
|
||||||
Confirmed on the PSP legacy-GL path specifically: there's no GPU buffer
|
|
||||||
re-upload cost (`meshFlushGL` is a literal no-op comment: "we use the
|
|
||||||
glClientState stuff" — `meshgl.c:91-93`; `meshDrawGL` just points
|
|
||||||
`glVertexPointer` at the CPU-side array each call), so the real cost is
|
|
||||||
(a) the CPU-side per-sprite vertex rewrite every frame and (b) GU
|
|
||||||
re-transforming the full vertex stream from RAM on every draw with no
|
|
||||||
skip for unchanged geometry.
|
|
||||||
|
|
||||||
**Fix direction** (already captured in `ROADMAP.md`'s debt backlog item 5):
|
|
||||||
extend `uiconsole.c`'s cached-mesh, dirty-flag-gated rebuild pattern to
|
|
||||||
the rest of the widget tree. This plan adds one refinement: also consider
|
|
||||||
packing `meshvertex_t` down (next item) since it multiplies this cost.
|
|
||||||
|
|
||||||
## Priority 4 — All-float vertex format wastes bandwidth and T&L cost
|
|
||||||
|
|
||||||
`meshvertex_t` uses two `float` fields (20 bytes) for every mesh and
|
|
||||||
sprite vertex, mesh-wide, with no normal or per-vertex color (color is
|
|
||||||
already handled at the material level, so that part is already optimal).
|
|
||||||
GU natively supports fixed-point 16-bit positions/UVs
|
|
||||||
(`GU_VERTEX_16BIT`/`GU_TEXTURE_16BIT`), which would roughly halve
|
|
||||||
per-vertex size and the corresponding vertex-fetch/transform cost, at the
|
|
||||||
cost of position precision (fine for UI/sprite work and most world
|
|
||||||
geometry at this engine's scale; worth checking against the largest
|
|
||||||
world coordinates actually used before committing). Pair with Priority 3
|
|
||||||
so the packing benefit compounds with the dirty-tracking benefit instead
|
|
||||||
of just reducing the cost of a rebuild that still happens every frame.
|
|
||||||
|
|
||||||
## Priority 5 — Running gameplay logic in JerryScript has a real per-frame cost on PSP
|
|
||||||
|
|
||||||
This is new since last session's work, not a pre-existing issue: `assets/
|
|
||||||
scripts/overworldscene.js`'s `update()` is now called every frame via
|
|
||||||
`scriptManagerCallGlobal("update")` (wired into `engine.c`'s
|
|
||||||
`engineUpdate()`), replacing what used to be a direct `cosf`/`sinf` C
|
|
||||||
callback (`entityUpdateAdd`). Per call, `scriptManagerCallGlobal`
|
|
||||||
(`scriptmanager.c:91-144`) does: a global-object property lookup (key is
|
|
||||||
cached, but the `jerry_object_get` dispatch still runs), full JS
|
|
||||||
interpreter call dispatch (frame setup, argument marshaling) for what's a
|
|
||||||
two-line function, and an unconditional `jerry_value_is_promise` check
|
|
||||||
every call even though `update()` is synchronous. Inside, `Math.cos`/
|
|
||||||
`Math.sin` run as JS builtin calls rather than direct libm calls.
|
|
||||||
|
|
||||||
Additionally: this project's JerryScript fork is already patched to use
|
|
||||||
32-bit float internally (`JERRY_NUMBER_TYPE_FLOAT64=0`,
|
|
||||||
`Findjerryscript.cmake`) — a deliberate, already-correct optimization for
|
|
||||||
this exact concern — but the *public* engine API (`jerry_value_as_number()`)
|
|
||||||
still returns `double`, so every native↔JS boundary crossing (every
|
|
||||||
`moduleBaseArgFloat`, every `moduleBaseVec3ToObject`) still pays a
|
|
||||||
float→double→float round trip. Also note `JERRY_MATH` is off, so
|
|
||||||
`Math.sin`/`cos` fall through to whatever generic libm the platform
|
|
||||||
provides rather than JerryScript's own fdlibm implementation — worth
|
|
||||||
checking whether turning it on changes anything measurable on PSP.
|
|
||||||
|
|
||||||
**This is one `update()` call for one scene-level script today — cheap in
|
|
||||||
absolute terms.** It becomes a real problem only if the pattern scales:
|
|
||||||
giving many individual entities their own per-frame JS update callback
|
|
||||||
would multiply all of the above per entity. **Recommendation: keep
|
|
||||||
high-frequency, hot per-entity logic (movement, camera math, physics
|
|
||||||
response) in native C update callbacks (`entityUpdateAdd`, the existing
|
|
||||||
mechanism), and reserve JS for one-time setup, infrequent/event-driven
|
|
||||||
logic, and coarse-grained per-scene orchestration** — which is exactly
|
|
||||||
what `require()` and the typed component wrappers built this session are
|
|
||||||
suited for, not a per-entity-per-frame hot path. This is a design
|
|
||||||
guideline to apply going forward, not a regression to fix in the current
|
|
||||||
`overworldscene.js` (one scene-level `update()` call per frame is fine).
|
|
||||||
|
|
||||||
## Priority 6 — No pooling/arena allocator; plain malloc/free everywhere
|
|
||||||
|
|
||||||
`memoryAllocate`/`memoryFree` (`util/memory.c`) are direct `malloc`/`free`
|
|
||||||
passthroughs (`memoryAlign`→`memalign`, `memoryReallocate`/`memoryResize`→
|
|
||||||
`realloc`), with the only extra behavior being a global allocation-count
|
|
||||||
tracker used solely for test leak detection. No pool, arena, free-list,
|
|
||||||
or size-class allocator exists anywhere.
|
|
||||||
|
|
||||||
This survey found **no rogue per-frame heap allocations** in the hot
|
|
||||||
paths checked (render dispatch in `entityrenderable.c`, all of
|
|
||||||
`physics/*.c`) — so this is not an active bug today. But it's a
|
|
||||||
structural risk for a no-VM platform over a long play session: any future
|
|
||||||
code that does frequent small alloc/free (dynamic lists, string
|
|
||||||
building, temp buffers) will fragment the 32-64MB heap with no OS-level
|
|
||||||
recovery mechanism. **Recommendation: before adding any new subsystem
|
|
||||||
that allocates/frees frequently at runtime (not just at load time),
|
|
||||||
default to a fixed-size pool or arena for it**, following the same
|
|
||||||
"static, bounded" philosophy already used for `ENTITY_COUNT_MAX`/
|
|
||||||
`SCENE_COUNT_MAX`/`ASSET_ENTRY_COUNT_MAX` elsewhere in the engine, rather
|
|
||||||
than reaching for `memoryAllocate` per-instance.
|
|
||||||
|
|
||||||
## Already correct — don't touch without new evidence
|
|
||||||
|
|
||||||
- **`entityposition_t`'s matrix caching** (the pattern the project owner
|
|
||||||
called out as the reason for writing this plan). Verified: a full
|
|
||||||
"dirty" recompute chain (decompose + rebuild local + rebuild world) is
|
|
||||||
~10 trig calls (`asinf`/`cosf`/`atan2f`) plus at most one 4x4 matrix
|
|
||||||
multiply, and `entityPositionEnsurePRS`/`EnsureLocal`/`EnsureWorld`
|
|
||||||
(`entityposition.c:596-669`) all early-return on a flag check, only
|
|
||||||
doing that work when something actually changed. The cache is correct
|
|
||||||
and clearly worth its ~128-byte-per-entity matrix storage cost.
|
|
||||||
- **Physics narrow-phase sqrt avoidance.** All three `sqrtf` call sites
|
|
||||||
in `physicstest.c`/`physicsshapemesh.c` already sit behind a
|
|
||||||
squared-distance early-reject and only compute the real (linear)
|
|
||||||
distance once, when actually needed for penetration depth — no further
|
|
||||||
"use squared distance instead" opportunity was found here.
|
|
||||||
- **No allocations in the render/physics hot loop** (Priority 6) — this
|
|
||||||
is good and worth preserving as new code is added to those files.
|
|
||||||
- **Asset decompression already runs off the main thread.** Assets are
|
|
||||||
DEFLATE-compressed (not stored) in `dusk.dsk`, so loading pays a real
|
|
||||||
CPU cost for decompression — but every `*LoaderAsync` function
|
|
||||||
(`asset/loader/*/*.c`) runs via `ASSET.loadThread`
|
|
||||||
(`assertNotMainThread` in `asset.c:365`), so this cost is already kept
|
|
||||||
off the main thread. Low priority to change; if load-time CPU cost
|
|
||||||
becomes a measured problem later, revisit stored-vs-compressed as a
|
|
||||||
build-time flag rather than assuming compression is free.
|
|
||||||
|
|
||||||
## Suggested approach
|
|
||||||
|
|
||||||
1. **Measure before changing.** None of the above have been profiled on
|
|
||||||
real PSP hardware in this pass — this is a source-level survey, not a
|
|
||||||
profile. Before investing in Priority 1 or 2 especially, confirm with
|
|
||||||
an actual PSP build/run (or at minimum the existing "Entity manager
|
|
||||||
size" startup log plus a frame-time counter) that these are the real
|
|
||||||
bottlenecks, not just the largest numbers on paper.
|
|
||||||
2. **Priority 1 first** — it's the most contained (one component's data
|
|
||||||
layout), has the clearest before/after metric (the startup log line),
|
|
||||||
and doesn't require new platform-specific code.
|
|
||||||
3. **Priority 3 next** (extend the console's dirty-tracking pattern to
|
|
||||||
the rest of the UI) — already scoped in `ROADMAP.md`, no new design
|
|
||||||
needed, just implementation.
|
|
||||||
4. **Priority 2 (VFPU) as a dedicated project**, not a quick pass — it's
|
|
||||||
the largest potential win but touches core math plumbing and needs
|
|
||||||
careful correctness verification on real hardware.
|
|
||||||
5. Treat Priority 5 as a standing design guideline for all future
|
|
||||||
scripting work, not a one-time fix.
|
|
||||||
-90
@@ -1,90 +0,0 @@
|
|||||||
# Dusk Roadmap
|
|
||||||
|
|
||||||
Tracking upcoming milestones for the engine. See `PSP_OPTIMIZATION_PLAN.md`
|
|
||||||
for a memory/CPU optimization survey specifically targeting the PSP build
|
|
||||||
(milestone 4 below is its Priority 3).
|
|
||||||
|
|
||||||
## Upcoming milestones
|
|
||||||
|
|
||||||
1. Add a very basic physics engine, moving away from the current
|
|
||||||
tile-based movement.
|
|
||||||
2. Give entities full freedom of movement (no longer locked to tile
|
|
||||||
grid positions).
|
|
||||||
3. Update entity interaction, triggers, chunk management, and other
|
|
||||||
systems that currently assume tile-based positioning so they work
|
|
||||||
with the new 3D positioning/movement code.
|
|
||||||
4. Investigate and fix poor UI rendering performance. Rendering the
|
|
||||||
console alone tanks framerate despite the existing mesh
|
|
||||||
optimizations, so there is likely more headroom to find in the
|
|
||||||
vertex/text rendering path.
|
|
||||||
5. Create UI elements for displaying status indicators, e.g. network
|
|
||||||
connection state and save-in-progress.
|
|
||||||
6. Fully test saving end-to-end on all supported platforms.
|
|
||||||
7. Remove the tile system from chunks in favor of meshes, with
|
|
||||||
dynamic hitboxes per chunk loaded in from the chunk file data.
|
|
||||||
8. Create UI elements for network status: a connecting modal, an
|
|
||||||
error state, and a connected flag. Retire the test HTTP request
|
|
||||||
once these are in place.
|
|
||||||
9. Build the socket server and client implementation, including
|
|
||||||
handlers for the different packet types.
|
|
||||||
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
|
|
||||||
the existing `npc` and `player` types. Limit to 8 (defined
|
|
||||||
constant) for now.
|
|
||||||
11. Send and receive `clientplayer` position over the network.
|
|
||||||
12. Create a UI menu for creating a server and joining a server. For
|
|
||||||
now, join IPs are hard-coded (testing against a fixed IP of
|
|
||||||
10.0.0.94).
|
|
||||||
13. Create "handshake" packets. For now, just send the username,
|
|
||||||
enforced to be under 10 characters long.
|
|
||||||
14. Server tracks all players' positions and broadcasts them to all
|
|
||||||
connected clients.
|
|
||||||
15. Server sends disconnect packets for users who leave.
|
|
||||||
16. Server assigns each client a UUID; all clients know every other
|
|
||||||
client's UUID (used to reference them across position updates,
|
|
||||||
disconnect packets, etc).
|
|
||||||
17. Server notifies all clients (by UUID) when a user joins, leaves,
|
|
||||||
or is disconnected, so clients can spawn or remove the
|
|
||||||
corresponding `clientplayer` entity in the world.
|
|
||||||
|
|
||||||
## Infrastructure / debt backlog
|
|
||||||
|
|
||||||
Found during a full-codebase inventory pass (2026-07-30, see `STATUS.md`
|
|
||||||
for the full survey). These aren't new feature milestones so much as
|
|
||||||
loose ends worth closing, roughly in order of how cheap/safe they are to
|
|
||||||
fix:
|
|
||||||
|
|
||||||
1. Re-enable `test/item` (currently commented out in
|
|
||||||
`test/CMakeLists.txt`) -- confirm it still passes and turn it back on.
|
|
||||||
2. Restore `itemgive.c/h` in `src/duskrpg/item/CMakeLists.txt` -- the
|
|
||||||
textbox UI dependency it was waiting on (`ui/textbox/`) is already
|
|
||||||
back.
|
|
||||||
3. Decide the fate of the `vita` target: `scripts/build-vita.sh` and
|
|
||||||
`docker/vita/` reference `-DDUSK_TARGET_SYSTEM=vita`, but no
|
|
||||||
`cmake/targets/vita.cmake` exists, so the build is currently broken.
|
|
||||||
Either implement it or remove the dangling scripts/Dockerfile.
|
|
||||||
4. Add per-PR CI build coverage for at least one non-Linux target (PSP,
|
|
||||||
Knulli, GameCube, Wii currently only build on tag push, so
|
|
||||||
regressions there are invisible until a release).
|
|
||||||
5. Tackle milestone 4 above (poor UI rendering performance) with a
|
|
||||||
concrete lead: extend `uiconsole.c`'s cached-mesh pattern (rebuild
|
|
||||||
only on dirty) to the general widget framework
|
|
||||||
(`uiframe.c`/`uilabel.c`/buttons/menus), which currently rebuilds and
|
|
||||||
re-uploads geometry via `spriteBatchBuffer`/`meshFlush` every frame.
|
|
||||||
6. Revisit the GameCube/Wii networking static-IP workaround in
|
|
||||||
`networkdolphin.c` -- it's standing in for an unresolved suspected
|
|
||||||
memory-corruption bug in `if_config()`'s DHCP path.
|
|
||||||
7. Decide whether the PSP dialog-based network connect UI needs to come
|
|
||||||
back -- it was removed entirely (not fixed) when the original
|
|
||||||
dialog-tearing bug proved hard to resolve.
|
|
||||||
8. Backfill unit tests for the biggest untested surfaces: `ui/` (whole
|
|
||||||
widget framework), `save/`, `script/` (JerryScript bindings), `event/`.
|
|
||||||
|
|
||||||
## Principles
|
|
||||||
|
|
||||||
- Never trust the network implicitly. Neither side (server or client)
|
|
||||||
should assume the other's packets are well-formed or benign --
|
|
||||||
validate all incoming packet data defensively, since either side
|
|
||||||
may send garbage or malicious data. Use `errorret_t` /
|
|
||||||
`errorThrow()` for these runtime checks, not assert macros --
|
|
||||||
asserts are debug-only and won't guard release builds against
|
|
||||||
malformed or malicious packet data.
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# Dusk Engine - Status Snapshot
|
|
||||||
|
|
||||||
This is a point-in-time inventory of the codebase's maturity, test coverage,
|
|
||||||
and known gaps. It is not auto-maintained -- re-survey periodically (or
|
|
||||||
whenever picking a new roadmap milestone) rather than trusting it blindly.
|
|
||||||
See `ROADMAP.md` for the ordered feature milestones this status feeds into,
|
|
||||||
and `CLAUDE.md` for coding conventions.
|
|
||||||
|
|
||||||
Last surveyed: 2026-07-30, at commit `e61914ba`.
|
|
||||||
|
|
||||||
## Core engine (`src/dusk/`)
|
|
||||||
|
|
||||||
| Subsystem | Maturity | Notes |
|
|
||||||
|------------|------------------------------------|-------|
|
|
||||||
| asset | Mature, fully wired | Model loading's "async" path is actually synchronous (`assetmodelloader.h`) -- only real stub found in core. |
|
|
||||||
| script | Mature for its current scope | Registered modules: `modulePlatform`, `moduleComponent`, `moduleEntity`, `moduleScene` only. No typed per-component JS wrappers (all components go through the generic `Component`). No unit tests. |
|
|
||||||
| entity | Mature | Engine-level prefab lists are empty sentinels; all real prefabs live in `duskrpg`. |
|
|
||||||
| scene | Mature | Same prefab-delegation pattern as entity. No JSON serialize/deserialize (removed by design). |
|
|
||||||
| save | Mature, but undocumented | Slot-based, yyjson + CRC32, platform stream hooks. Not covered in `CLAUDE.md`, no tests. |
|
|
||||||
| network | Connection-state layer only | HTTP client + connection state machine; no multiplayer/replication protocol (expected -- that's roadmap items 8-17). Not covered in `CLAUDE.md`, has HTTP tests only. |
|
|
||||||
| physics | Mature, recently churned | Recent revert/re-disable of "old ent code" suggests component wiring around physics isn't fully settled. Well tested. |
|
|
||||||
| animation | Early/mid-stage | Keyframes + easing only, no blend trees or state machines. Terse commit history ("ANIM") suggests still iterating. |
|
|
||||||
| display | Most mature/battle-tested | Backbone of the engine; dominated by platform optimization commits. |
|
|
||||||
| ui | Actively churning | Widget framework has had features added and ripped out repeatedly (story/battle UI added then removed). Rendering path is the likely root cause of roadmap item 4 (see below). No tests at all. |
|
|
||||||
| console | Small, finished for its scope | Already has the cached-mesh optimization pattern the rest of `ui/` lacks. |
|
|
||||||
| event | Clean, small, finished | Pub/sub, rebuilt to replace the old input system. |
|
|
||||||
| game | Intentionally header-only | Real implementation lives in `duskrpg/game/game.c`. |
|
|
||||||
|
|
||||||
### UI rendering performance (roadmap item 4)
|
|
||||||
Confirmed root-cause candidate: `src/dusk/ui/debug/uiconsole.c` caches a
|
|
||||||
persistent mesh and only rebuilds on dirty/scroll change. The rest of the
|
|
||||||
widget framework (`uiframe.c`, `uilabel.c`, buttons, menus, settings
|
|
||||||
screens) still goes through `spriteBatchBuffer`/`spriteBatchFlush` and
|
|
||||||
re-uploads vertex data via `meshFlush` every frame, with a full flush
|
|
||||||
forced on every shader/material change. This is almost certainly what
|
|
||||||
"tanks framerate" on PSP. Fix direction: extend the console's cached-mesh
|
|
||||||
pattern to the general widget path (rebuild only on dirty, not every
|
|
||||||
frame).
|
|
||||||
|
|
||||||
## Game layer (`src/duskrpg/`)
|
|
||||||
|
|
||||||
Actively maintained, not orphaned (despite an old "remove rpg" commit deep
|
|
||||||
in history) -- last touched the same day as this survey.
|
|
||||||
|
|
||||||
- **cutscene/** -- actively developed, matches `CLAUDE.md`'s documented
|
|
||||||
recipe exactly. 16 registered item types.
|
|
||||||
- **entity/, scene/** -- overworld player/camera/interactable components
|
|
||||||
and prefabs, active.
|
|
||||||
- **item/** -- `item.c`/`inventory.c`/`backpack.c` built via a working
|
|
||||||
`item.json` -> `itemdef.h` codegen pipeline. `itemgive.c/h` exist on
|
|
||||||
disk but are explicitly excluded from the CMake build pending textbox
|
|
||||||
UI restoration -- **that UI (`ui/textbox/`) is already restored**, so
|
|
||||||
this looks like an overdue follow-up, not a real blocker.
|
|
||||||
- **input/** -- headers only, no implementation. Contains the one
|
|
||||||
genuine TODO found in the whole `duskrpg` tree: `// TODO: Wiimote, USB
|
|
||||||
Keyboard, probably more.`
|
|
||||||
- **ui/** -- textbox restored and built; `uitestlabel.c/h` is an
|
|
||||||
intentional smoke-test scaffold, not dead code.
|
|
||||||
|
|
||||||
## Platform layers
|
|
||||||
|
|
||||||
| Platform | Status |
|
|
||||||
|--------------|--------|
|
|
||||||
| duskgl / dusksdl2 | Complete, shared by Linux/Knulli/PSP, no gaps found. |
|
|
||||||
| dusklinux | Complete, well-trodden. |
|
|
||||||
| duskpsp | Complete for current design. The historical dialog/tearing bug (see memory `project_psp_dialog_tearing` etc.) was **not fixed -- the dialog-based connect UI was removed entirely** (commit `d7982599`, "Simplified PSP network") in favor of a silent profile-based connect. Revisit if the dialog UX is still wanted. |
|
|
||||||
| duskdolphin (GameCube/Wii) | Functional, not a stub -- real GX-based display/mesh/shader/texture. Wii input uses only the GameCube `PAD_*` library; no WPAD/Wiimote support (`inputdolphin.h` has `#error "Wii not implemented"` gated behind macros that are never defined, so currently dormant, not a live build break). `networkdolphin.c` hardcodes a static IP as an explicit temporary workaround for a suspected DHCP-related memory-corruption bug in `if_config()` -- root cause still open. |
|
|
||||||
| vita | **Referenced by `scripts/build-vita.sh` and `docker/vita/` but no `cmake/targets/vita.cmake` exists** -- the build target is broken/unfinished at the CMake level. |
|
|
||||||
|
|
||||||
### CI coverage gap
|
|
||||||
`.github/workflows/test.yml` only builds+tests Linux, on PRs to `main`.
|
|
||||||
`build.yml` builds all other platforms (PSP, Knulli, GameCube, Wii +
|
|
||||||
ISO variants) but **only on tag push** (release time). Vita and Dolphin
|
|
||||||
aren't in CI at all. Net effect: regressions on 4+ platforms can land
|
|
||||||
silently until a release tag is cut.
|
|
||||||
|
|
||||||
## Test coverage gaps
|
|
||||||
|
|
||||||
Core `src/dusk/` subsystems with **zero** unit tests: `console`,
|
|
||||||
`engine`, `event`, `game`, `input`, `log`, `save`, `script` (+ all JS
|
|
||||||
module bindings), `system`, `ui` (entire widget framework).
|
|
||||||
|
|
||||||
`src/duskrpg/` has almost no test coverage: `test/item/test_inventory.c`
|
|
||||||
exists but **`add_subdirectory(item)` is commented out in
|
|
||||||
`test/CMakeLists.txt`**, so even that one test never runs. `cutscene`,
|
|
||||||
`entity`, `game`, `input`, `scene`, `ui` under `duskrpg` have no tests at
|
|
||||||
all.
|
|
||||||
|
|
||||||
No stale tests were found referencing deleted systems (old JSON
|
|
||||||
serialize/deserialize, old event/cutscene modules) -- the test suite is
|
|
||||||
internally consistent with current code, just incomplete in coverage.
|
|
||||||
|
|
||||||
## Build/tooling gaps worth knowing about
|
|
||||||
|
|
||||||
- `assetsraw/` -> `assets/` (chunk JSON -> `.dcf`) is a manual/editor-only
|
|
||||||
step (`tools.asset.chunk`), not part of the CMake build -- committed
|
|
||||||
`.dcf` files can silently drift from their raw source.
|
|
||||||
- Several Python tool packages exist but aren't wired into any build:
|
|
||||||
`tools/color/csv/`, `tools/asset/chunk_json/`, `tools/asset/dmf/`,
|
|
||||||
`tools/asset/tiles/`, `tools/input/csv/`. Some may be superseded by
|
|
||||||
`tools/color.py`/`tools/item.py`; worth a pass to confirm which are
|
|
||||||
live vs leftover.
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
add_subdirectory(debug)
|
||||||
|
add_subdirectory(frame)
|
||||||
|
add_subdirectory(focus)
|
||||||
|
add_subdirectory(overlay)
|
||||||
|
add_subdirectory(transition)
|
||||||
|
add_subdirectory(widget)
|
||||||
|
|
||||||
|
# Sources
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
ui.c
|
||||||
|
uielement.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "ui/uielement.h"
|
||||||
|
#include "ui/focus/uifocus.h"
|
||||||
|
|
||||||
|
ui_t UI;
|
||||||
|
|
||||||
|
errorret_t uiInit(void) {
|
||||||
|
memoryZero(&UI, sizeof(ui_t));
|
||||||
|
uiFocusInit();
|
||||||
|
uiElementsSort();
|
||||||
|
|
||||||
|
uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementInit(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiUpdate(void) {
|
||||||
|
uiFocusUpdate();
|
||||||
|
|
||||||
|
uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementUpdate(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiRender(void) {
|
||||||
|
const uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementDraw(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiDispose(void) {
|
||||||
|
uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementDispose(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *nothing;
|
||||||
|
} ui_t;
|
||||||
|
|
||||||
|
extern ui_t UI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the UI system.
|
||||||
|
*/
|
||||||
|
errorret_t uiInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the UI system.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiUpdate(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the UI system.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiRender(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the UI system.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiDispose(void);
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uielement.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/sort.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
#include "ui/debug/uifps.h"
|
||||||
|
#include "engine/engine.h"
|
||||||
|
#include "ui/overlay/uifullbox.h"
|
||||||
|
#include "ui/overlay/uiloading.h"
|
||||||
|
#include "ui/overlay/uicrop.h"
|
||||||
|
#include "ui/transition/uitransition.h"
|
||||||
|
#include "ui/debug/uiconsole.h"
|
||||||
|
#include "ui/frame/uiconfirm.h"
|
||||||
|
|
||||||
|
// Priming pass: X does nothing here, so this only exists to process any
|
||||||
|
// #include directives nested in uielementlist.h/ui/uielementgame.h at
|
||||||
|
// file scope (each such header's own #pragma once makes it a no-op on
|
||||||
|
// the real pass below, which happens inside UI_ELEMENTS[]'s braces,
|
||||||
|
// where a raw #include of a declaration would be invalid).
|
||||||
|
#define X(initFn, updateFn, drawFn, disposeFn, order) // do nothing
|
||||||
|
#include "uielementlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
uielement_t UI_ELEMENTS[] = {
|
||||||
|
#define X(initFn, updateFn, drawFn, disposeFn, elementOrder) \
|
||||||
|
{ \
|
||||||
|
.init = initFn, .update = updateFn, .draw = drawFn, \
|
||||||
|
.dispose = disposeFn, .order = elementOrder \
|
||||||
|
},
|
||||||
|
#include "uielementlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
{ 0 } // Null terminator
|
||||||
|
};
|
||||||
|
|
||||||
|
bool_t uiElementIsNull(const uielement_t *element) {
|
||||||
|
return element->init == NULL &&
|
||||||
|
element->update == NULL &&
|
||||||
|
element->draw == NULL &&
|
||||||
|
element->dispose == NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int_t uiElementCompareOrder(const void *a, const void *b) {
|
||||||
|
const uielement_t *elementA = (const uielement_t *)a;
|
||||||
|
const uielement_t *elementB = (const uielement_t *)b;
|
||||||
|
if(elementA->order < elementB->order) return -1;
|
||||||
|
if(elementA->order > elementB->order) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiElementsSort(void) {
|
||||||
|
// The trailing null terminator is always the last static entry -- sort
|
||||||
|
// everything before it, and leave it in place.
|
||||||
|
const size_t count = (sizeof(UI_ELEMENTS) / sizeof(UI_ELEMENTS[0])) - 1;
|
||||||
|
sortBubble(UI_ELEMENTS, count, sizeof(uielement_t), uiElementCompareOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementInit(uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->init != NULL) errorChain(element->init());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementUpdate(uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->update != NULL) errorChain(element->update());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementDraw(const uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->draw != NULL) errorChain(element->draw());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementDispose(uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->dispose != NULL) errorChain(element->dispose());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
// Built-in order tiers. Lower values update/render first; ties preserve
|
||||||
|
// their relative UI_ELEMENTS declaration order (uiElementsSort uses a
|
||||||
|
// stable sort). Game-specific elements can use any int32_t value -- these
|
||||||
|
// are just the ones the engine itself relies on.
|
||||||
|
#define UI_ELEMENT_ORDER_DEFAULT 0
|
||||||
|
#define UI_ELEMENT_ORDER_DEBUG 1000
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
errorret_t (*init)();
|
||||||
|
errorret_t (*update)();
|
||||||
|
errorret_t (*draw)();
|
||||||
|
errorret_t (*dispose)();
|
||||||
|
int32_t order;
|
||||||
|
} uielement_t;
|
||||||
|
|
||||||
|
extern uielement_t UI_ELEMENTS[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when all four callbacks on the element are NULL,
|
||||||
|
* which marks the end of the UI_ELEMENTS array.
|
||||||
|
*
|
||||||
|
* @param element The element to test.
|
||||||
|
* @returns True if the element is the null terminator.
|
||||||
|
*/
|
||||||
|
bool_t uiElementIsNull(const uielement_t *element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two elements by their .order field, ascending. Matches
|
||||||
|
* sortcompare_t, for use with the project's sort utilities.
|
||||||
|
*
|
||||||
|
* @param a First uielement_t to compare.
|
||||||
|
* @param b Second uielement_t to compare.
|
||||||
|
* @return Negative if a < b, zero if a == b, positive if a > b.
|
||||||
|
*/
|
||||||
|
int_t uiElementCompareOrder(const void *a, const void *b);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stably sorts UI_ELEMENTS in place by .order, ascending. The trailing
|
||||||
|
* null terminator is never moved. Called once by uiInit -- element order
|
||||||
|
* is static after that, so there's no need to re-sort every frame.
|
||||||
|
*/
|
||||||
|
void uiElementsSort(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a UI element, invoking its init callback if set.
|
||||||
|
*
|
||||||
|
* @param element The element to initialize.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiElementInit(uielement_t *element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a UI element, calling its update callback if set.
|
||||||
|
*
|
||||||
|
* @param element The element to update.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiElementUpdate(uielement_t *element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a UI element, calling its draw callback if set.
|
||||||
|
*
|
||||||
|
* @param element The element to render.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiElementDraw(const uielement_t *element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of a UI element, invoking its dispose callback if set.
|
||||||
|
*
|
||||||
|
* @param element The element to dispose.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiElementDispose(uielement_t *element);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
uibutton.c
|
||||||
|
uicheckbox.c
|
||||||
|
uitab.c
|
||||||
|
uislider.c
|
||||||
|
uidropdown.c
|
||||||
|
uiscrolling.c
|
||||||
|
uimenu.c
|
||||||
|
uilabel.c
|
||||||
|
uiwidgetlabel.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uilabel.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
|
||||||
|
void uiLabelInit(uilabel_t *label, font_t *font) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(font, "Font cannot be NULL");
|
||||||
|
|
||||||
|
memoryZero(label, sizeof(uilabel_t));
|
||||||
|
label->font = font;
|
||||||
|
label->color = COLOR_WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiLabelSetText(uilabel_t *label, const char_t *text) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(text, "Text cannot be NULL");
|
||||||
|
assertStrLenMax(text, UI_LABEL_TEXT_MAX, "Label text too long");
|
||||||
|
|
||||||
|
stringCopy(label->text, text, UI_LABEL_TEXT_MAX);
|
||||||
|
label->spriteCount = textBuildSpriteCache(
|
||||||
|
label->text, label->font, label->sprites, UI_LABEL_SPRITE_COUNT_MAX,
|
||||||
|
&label->width, &label->height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiLabelSetColor(uilabel_t *label, const color_t color) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
label->color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiLabelGetSize(
|
||||||
|
const uilabel_t *label,
|
||||||
|
int32_t *outWidth,
|
||||||
|
int32_t *outHeight
|
||||||
|
) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(outWidth, "Output width cannot be NULL");
|
||||||
|
assertNotNull(outHeight, "Output height cannot be NULL");
|
||||||
|
*outWidth = label->width;
|
||||||
|
*outHeight = label->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiLabelDraw(
|
||||||
|
const uilabel_t *label,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
if(label->spriteCount == 0) errorOk();
|
||||||
|
|
||||||
|
// Cached sprites are relative to (0,0); textDrawSpriteCache translates
|
||||||
|
// into the requested screen position here instead of in
|
||||||
|
// uiLabelSetText, so a label can be repositioned every frame without
|
||||||
|
// rebuilding the (much more expensive) glyph/UV cache.
|
||||||
|
spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX];
|
||||||
|
errorChain(textDrawSpriteCache(
|
||||||
|
label->sprites, label->spriteCount, scratch, x, y, label->color,
|
||||||
|
label->font->texture
|
||||||
|
));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "display/text/font.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
|
||||||
|
#define UI_LABEL_TEXT_MAX 256
|
||||||
|
#define UI_LABEL_SPRITE_COUNT_MAX UI_LABEL_TEXT_MAX
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t text[UI_LABEL_TEXT_MAX];
|
||||||
|
color_t color;
|
||||||
|
font_t *font;
|
||||||
|
|
||||||
|
spritebatchsprite_t sprites[UI_LABEL_SPRITE_COUNT_MAX];
|
||||||
|
uint32_t spriteCount;
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
} uilabel_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a label, defaulting to white text and no text set.
|
||||||
|
*
|
||||||
|
* @param label The label to initialize.
|
||||||
|
* @param font Font to use for rendering. Must outlive the label.
|
||||||
|
*/
|
||||||
|
void uiLabelInit(uilabel_t *label, font_t *font);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the label's text, rebuilding its cached sprites (glyph lookup, UV,
|
||||||
|
* and layout) immediately. This is the only way the sprite cache gets
|
||||||
|
* rebuilt -- uiLabelDraw never recomputes it, so call this whenever the
|
||||||
|
* text changes rather than once up front and expecting it to stay in sync.
|
||||||
|
*
|
||||||
|
* @param label The label to update.
|
||||||
|
* @param text Null-terminated string to display. Must be shorter than
|
||||||
|
* UI_LABEL_TEXT_MAX.
|
||||||
|
*/
|
||||||
|
void uiLabelSetText(uilabel_t *label, const char_t *text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the label's tint color. Cheap -- doesn't touch the sprite cache,
|
||||||
|
* since color is applied via the draw material, not baked into sprites.
|
||||||
|
*
|
||||||
|
* @param label The label to update.
|
||||||
|
* @param color The new tint color.
|
||||||
|
*/
|
||||||
|
void uiLabelSetColor(uilabel_t *label, const color_t color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the measured size (in pixels) of the label's current text, cached
|
||||||
|
* from the last uiLabelSetText call.
|
||||||
|
*
|
||||||
|
* @param label The label to query.
|
||||||
|
* @param outWidth Pointer to store the width.
|
||||||
|
* @param outHeight Pointer to store the height.
|
||||||
|
*/
|
||||||
|
void uiLabelGetSize(
|
||||||
|
const uilabel_t *label,
|
||||||
|
int32_t *outWidth,
|
||||||
|
int32_t *outHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the label's cached sprites at the given screen position in a
|
||||||
|
* single batched buffer call. Does not recompute glyph layout -- call
|
||||||
|
* uiLabelSetText first whenever the text changes.
|
||||||
|
*
|
||||||
|
* @param label The label to draw.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiLabelDraw(
|
||||||
|
const uilabel_t *label,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
);
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user