Scene.set() JS lifecycle, UI sprite caching, emulator test scripts
- Add Scene.set(module) so JerryScript can switch scenes via
require()'d {init, update, dispose} modules; wire it into
engineUpdate() and rework overworldscene.js/init.js to the new shape.
- Fix scriptManagerExecFile() never pushing its own file's directory
onto the require() dir stack, breaking relative require() calls from
a top-level entry script (e.g. init.js -> ./overworldscene.js).
- Cache sprite geometry across frames for uislider/uitab/uiframe
(dialogs/textbox/settings) and give uitextbox a per-page glyph cache
instead of one spriteBatchBuffer call per visible character. uiconsole
drops its alloc/free vertex buffer for a fixed-size one. uifps skips
rebuilding its label when the text hasn't changed.
- Add PSP_OPTIMIZATION_PLAN.md and a handful of UI/spritebatch unit
tests covering the new caching logic.
- Add Dolphin/PPSSPP emulator smoke-test scripts (+ Docker variants and
CI jobs) for GameCube/Wii/PSP.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,3 +33,74 @@ jobs:
|
|||||||
libssl-dev
|
libssl-dev
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: ./scripts/test-linux.sh
|
run: ./scripts/test-linux.sh
|
||||||
|
|
||||||
|
# Emulator smoke tests: boot the built disc/EBOOT for a fixed window and
|
||||||
|
# confirm the emulator doesn't crash. Not yet verified against a real
|
||||||
|
# runner (no CI run has exercised these) -- continue-on-error so a
|
||||||
|
# flaky/broken emulator step doesn't block the required Linux test job.
|
||||||
|
run-tests-gamecube-dolphin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ghcr.io/extremscorner/libogc2:latest
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js
|
||||||
|
run: apt-get update && apt-get install -y nodejs
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install additional dependencies
|
||||||
|
run: |
|
||||||
|
apt-get install -y \
|
||||||
|
python3-pip python3-polib python3-pil \
|
||||||
|
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||||
|
dolphin-emu xvfb
|
||||||
|
dkp-pacman -Syu --noconfirm
|
||||||
|
dkp-pacman -S --needed --noconfirm \
|
||||||
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
|
- name: Build GameCube ISO and boot it in Dolphin
|
||||||
|
run: ./scripts/test-gamecube-dolphin.sh
|
||||||
|
|
||||||
|
run-tests-wii-dolphin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ghcr.io/extremscorner/libogc2:latest
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js
|
||||||
|
run: apt-get update && apt-get install -y nodejs
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install additional dependencies
|
||||||
|
run: |
|
||||||
|
apt-get install -y \
|
||||||
|
python3-pip python3-polib python3-pil \
|
||||||
|
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||||
|
dolphin-emu xvfb
|
||||||
|
dkp-pacman -Syu --noconfirm
|
||||||
|
dkp-pacman -S --needed --noconfirm \
|
||||||
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
|
- name: Build Wii ISO and boot it in Dolphin
|
||||||
|
run: ./scripts/test-wii-dolphin.sh
|
||||||
|
|
||||||
|
run-tests-psp-ppsspp:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup pspdev
|
||||||
|
uses: ./.github/actions/setup-pspdev
|
||||||
|
- name: Install PPSSPPHeadless build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y git cmake ninja-build libsdl2-dev zlib1g-dev
|
||||||
|
- name: Build PPSSPPHeadless
|
||||||
|
run: |
|
||||||
|
git clone --recursive --depth 1 https://github.com/hrydgard/ppsspp.git /tmp/ppsspp
|
||||||
|
cmake -S /tmp/ppsspp -B /tmp/ppsspp/build -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build /tmp/ppsspp/build --target PPSSPPHeadless -- -j$(nproc)
|
||||||
|
echo "PPSSPP_HEADLESS_BIN=/tmp/ppsspp/build/PPSSPPHeadless" >> "$GITHUB_ENV"
|
||||||
|
- name: Build PSP EBOOT and boot it in PPSSPPHeadless
|
||||||
|
run: ./scripts/test-psp-ppsspp.sh
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# 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.
|
||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
# Dusk Roadmap
|
# Dusk Roadmap
|
||||||
|
|
||||||
Tracking upcoming milestones for the engine.
|
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
|
## Upcoming milestones
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// Entry point run once at startup (see game.c). Swap which scene module
|
||||||
|
// gets passed to Scene.set() here to change what the game boots into.
|
||||||
|
var overworldScene = require('./overworldscene.js');
|
||||||
|
Scene.set(overworldScene);
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
// Radius must stay well outside the floor's footprint (a 20x20 plane has a
|
// Radius must stay well outside the floor's footprint (a 20x20 plane has a
|
||||||
// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that
|
// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that
|
||||||
// puts parts of the floor's own geometry near/behind the camera's view
|
// puts parts of the floor's own geometry near/behind the camera's view
|
||||||
@@ -24,49 +29,57 @@ function updateCameraOrbit() {
|
|||||||
cameraPosition.lookAt(eyeX, eyeY, eyeZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
|
cameraPosition.lookAt(eyeX, eyeY, eyeZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called once per engine frame (see engineUpdate() -> scriptManagerCall
|
module.exports = {
|
||||||
// Global("update")).
|
// Called once by Scene.set(), right after it creates and activates the
|
||||||
function update() {
|
// scene this module owns.
|
||||||
if(cameraPosition) updateCameraOrbit();
|
init: function() {
|
||||||
}
|
// Camera, orbiting the origin (see updateCameraOrbit above).
|
||||||
|
var camera = new Entity();
|
||||||
|
cameraPosition = camera.add(POSITION);
|
||||||
|
camera.add(CAMERA);
|
||||||
|
updateCameraOrbit();
|
||||||
|
|
||||||
(function setup() {
|
// Static ground plane. Physics ignores the entity's position
|
||||||
var scene = new Scene();
|
// component -- the shape's own normal/distance fully define the
|
||||||
scene.setActive();
|
// plane in world space.
|
||||||
|
var plane = new Entity();
|
||||||
|
var planePosition = plane.add(POSITION);
|
||||||
|
planePosition.setLocalPosition(-10.0, 0.0, -10.0);
|
||||||
|
planePosition.setLocalScale(20.0, 1.0, 20.0);
|
||||||
|
|
||||||
// Camera, orbiting the origin (see updateCameraOrbit above).
|
var planePhysics = plane.add(PHYSICS);
|
||||||
var camera = new Entity();
|
planePhysics.setBodyType(PHYSICS_BODY_STATIC);
|
||||||
cameraPosition = camera.add(POSITION);
|
planePhysics.setShape(PHYSICS_SHAPE_PLANE, 0.0, 1.0, 0.0, 0.0);
|
||||||
camera.add(CAMERA);
|
|
||||||
updateCameraOrbit();
|
|
||||||
|
|
||||||
// Static ground plane. Physics ignores the entity's position component --
|
var planeRenderable = plane.add(RENDERABLE);
|
||||||
// the shape's own normal/distance fully define the plane in world space.
|
planeRenderable.setMesh(0, MESH_PLANE);
|
||||||
var plane = new Entity();
|
planeRenderable.setColor(128, 128, 128, 255);
|
||||||
var planePosition = plane.add(POSITION);
|
|
||||||
planePosition.setLocalPosition(-10.0, 0.0, -10.0);
|
|
||||||
planePosition.setLocalScale(20.0, 1.0, 20.0);
|
|
||||||
|
|
||||||
var planePhysics = plane.add(PHYSICS);
|
// Player: dynamic capsule body, moved relative to the camera by
|
||||||
planePhysics.setBodyType(PHYSICS_BODY_STATIC);
|
// PLAYER's own update callback (see entityplayer.c).
|
||||||
planePhysics.setShape(PHYSICS_SHAPE_PLANE, 0.0, 1.0, 0.0, 0.0);
|
var player = new Entity();
|
||||||
|
var playerPosition = player.add(POSITION);
|
||||||
|
playerPosition.setLocalPosition(0.0, 2.0, 0.0);
|
||||||
|
|
||||||
var planeRenderable = plane.add(RENDERABLE);
|
var playerPhysics = player.add(PHYSICS);
|
||||||
planeRenderable.setMesh(0, MESH_PLANE);
|
playerPhysics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5);
|
||||||
planeRenderable.setColor(128, 128, 128, 255);
|
|
||||||
|
|
||||||
// Player: dynamic capsule body, moved relative to the camera by
|
var playerRenderable = player.add(RENDERABLE);
|
||||||
// PLAYER's own update callback (see entityplayer.c).
|
playerRenderable.setMesh(0, MESH_CAPSULE);
|
||||||
var player = new Entity();
|
playerRenderable.setColor(0, 0, 255, 255);
|
||||||
var playerPosition = player.add(POSITION);
|
|
||||||
playerPosition.setLocalPosition(0.0, 2.0, 0.0);
|
|
||||||
|
|
||||||
var playerPhysics = player.add(PHYSICS);
|
player.add(PLAYER);
|
||||||
playerPhysics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5);
|
},
|
||||||
|
|
||||||
var playerRenderable = player.add(RENDERABLE);
|
// Called once per engine frame while this module is the active scene
|
||||||
playerRenderable.setMesh(0, MESH_CAPSULE);
|
// (see Scene.set(), engineUpdate() -> moduleSceneUpdateCurrent()).
|
||||||
playerRenderable.setColor(0, 0, 255, 255);
|
update: function() {
|
||||||
|
if(cameraPosition) updateCameraOrbit();
|
||||||
|
},
|
||||||
|
|
||||||
player.add(PLAYER);
|
// Called once by Scene.set() when this module is replaced by another,
|
||||||
})();
|
// right before the scene it owns is destroyed.
|
||||||
|
dispose: function() {
|
||||||
|
cameraPosition = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM ghcr.io/extremscorner/libogc2
|
||||||
|
WORKDIR /workdir
|
||||||
|
RUN apt update && \
|
||||||
|
dkp-pacman -Syu --noconfirm && \
|
||||||
|
apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl xorriso dolphin-emu xvfb && \
|
||||||
|
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip libogc2 gamecube-tools ppc-libmad ppc-zlib-ng ppc-liblzma ppc-bzip2 ppc-zstd
|
||||||
|
VOLUME ["/workdir"]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
FROM pspdev/pspdev:latest
|
||||||
|
WORKDIR /workdir
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
python3 \
|
||||||
|
py3-pip \
|
||||||
|
py3-dotenv \
|
||||||
|
git \
|
||||||
|
cmake \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
sdl2-dev \
|
||||||
|
zlib-dev \
|
||||||
|
linux-headers
|
||||||
|
|
||||||
|
# PPSSPP has no Alpine/Linux distro package -- build the "PPSSPPHeadless"
|
||||||
|
# target (the CLI/no-window build PPSSPP's own CI uses for automated runs)
|
||||||
|
# from source instead. The target name and exact CMake options have moved
|
||||||
|
# around across PPSSPP versions -- if this breaks, check
|
||||||
|
# https://github.com/hrydgard/ppsspp's CMakeLists.txt for the current name.
|
||||||
|
RUN git clone --recursive --depth 1 \
|
||||||
|
https://github.com/hrydgard/ppsspp.git /opt/ppsspp && \
|
||||||
|
cd /opt/ppsspp && \
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=Release && \
|
||||||
|
cmake --build build --target PPSSPPHeadless -- -j$(nproc) && \
|
||||||
|
cp build/PPSSPPHeadless /usr/local/bin/PPSSPPHeadless && \
|
||||||
|
rm -rf /opt/ppsspp
|
||||||
|
|
||||||
|
VOLUME ["/workdir"]
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t dusk-dolphin-test -f docker/dolphin-test/Dockerfile .
|
||||||
|
docker run --rm -v "$(pwd):/workdir" dusk-dolphin-test /bin/bash -c "./scripts/test-gamecube-dolphin.sh"
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$DEVKITPRO" ]; then
|
||||||
|
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOLPHIN_BIN="${DOLPHIN_BIN:-dolphin-emu}"
|
||||||
|
if ! command -v "$DOLPHIN_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "$DOLPHIN_BIN not found. Install Dolphin Emulator (e.g. 'apt install dolphin-emu')"
|
||||||
|
echo "or set DOLPHIN_BIN to its path."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v xvfb-run >/dev/null 2>&1; then
|
||||||
|
echo "xvfb-run not found. Install it (e.g. 'apt install xvfb') -- Dolphin's"
|
||||||
|
echo "Qt frontend needs a display even in batch mode."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
./scripts/build-gamecube-iso.sh
|
||||||
|
|
||||||
|
DUSK_TEST_DOLPHIN_SECONDS="${DUSK_TEST_DOLPHIN_SECONDS:-20}"
|
||||||
|
ISO="build-gamecube-iso/Dusk-NTSC-U.iso"
|
||||||
|
|
||||||
|
echo "Booting $ISO in Dolphin for up to ${DUSK_TEST_DOLPHIN_SECONDS}s (batch mode, headless via Xvfb)..."
|
||||||
|
|
||||||
|
# Dolphin's batch mode (-b -e) runs the disc until told to stop -- there's
|
||||||
|
# no "the game exited cleanly" signal to wait for, so we bound it with
|
||||||
|
# timeout and treat "still running when the clock ran out" as success.
|
||||||
|
set +e
|
||||||
|
xvfb-run -a timeout "${DUSK_TEST_DOLPHIN_SECONDS}" "$DOLPHIN_BIN" -b -e "$ISO"
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# timeout returns 124 when it had to kill Dolphin after the duration
|
||||||
|
# elapsed -- that means the disc booted and kept running, the expected
|
||||||
|
# smoke-test-passed outcome, not a failure. Any other non-zero status
|
||||||
|
# means Dolphin crashed or refused to boot the disc.
|
||||||
|
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
|
||||||
|
echo "Dolphin exited with unexpected status $status -- treating as a failure."
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "GameCube smoke test passed (Dolphin ran $ISO for ${DUSK_TEST_DOLPHIN_SECONDS}s without crashing)."
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t dusk-psp-test -f docker/psp-test/Dockerfile .
|
||||||
|
docker run --rm -v "$(pwd):/workdir" dusk-psp-test /bin/bash -c "./scripts/test-psp-ppsspp.sh"
|
||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$PSPDEV" ]; then
|
||||||
|
echo "PSPDEV environment variable is not set. Please set it to the path of your PSP development environment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PPSSPP doesn't ship a standard Linux package -- build its "PPSSPPHeadless"
|
||||||
|
# target from source (https://github.com/hrydgard/ppsspp) and either put it
|
||||||
|
# on PATH or point PPSSPP_HEADLESS_BIN at it. See docker/psp-test/Dockerfile
|
||||||
|
# for a from-scratch build.
|
||||||
|
PPSSPP_HEADLESS_BIN="${PPSSPP_HEADLESS_BIN:-PPSSPPHeadless}"
|
||||||
|
if ! command -v "$PPSSPP_HEADLESS_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "$PPSSPP_HEADLESS_BIN not found. Build PPSSPP's 'PPSSPPHeadless' target"
|
||||||
|
echo "from source or set PPSSPP_HEADLESS_BIN to its path."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
./scripts/build-psp.sh
|
||||||
|
|
||||||
|
DUSK_TEST_PPSSPP_SECONDS="${DUSK_TEST_PPSSPP_SECONDS:-20}"
|
||||||
|
EBOOT="build-psp/EBOOT.PBP"
|
||||||
|
|
||||||
|
echo "Booting $EBOOT in PPSSPPHeadless (timeout ${DUSK_TEST_PPSSPP_SECONDS}s)..."
|
||||||
|
|
||||||
|
# PPSSPPHeadless is purpose-built for automated/CI runs -- it renders
|
||||||
|
# without a window and is expected to exit on its own once --timeout
|
||||||
|
# elapses, unlike Dolphin's batch mode which has to be killed externally.
|
||||||
|
# Check `"$PPSSPP_HEADLESS_BIN" --help` if these flags don't match your
|
||||||
|
# PPSSPP checkout -- the headless CLI has changed across versions.
|
||||||
|
set +e
|
||||||
|
"$PPSSPP_HEADLESS_BIN" --timeout="${DUSK_TEST_PPSSPP_SECONDS}" "$EBOOT"
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$status" -ne 0 ]; then
|
||||||
|
echo "PPSSPPHeadless exited with status $status"
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PSP smoke test passed (PPSSPPHeadless ran EBOOT.PBP for ${DUSK_TEST_PPSSPP_SECONDS}s without crashing)."
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t dusk-dolphin-test -f docker/dolphin-test/Dockerfile .
|
||||||
|
docker run --rm -v "$(pwd):/workdir" dusk-dolphin-test /bin/bash -c "./scripts/test-wii-dolphin.sh"
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$DEVKITPRO" ]; then
|
||||||
|
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOLPHIN_BIN="${DOLPHIN_BIN:-dolphin-emu}"
|
||||||
|
if ! command -v "$DOLPHIN_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "$DOLPHIN_BIN not found. Install Dolphin Emulator (e.g. 'apt install dolphin-emu')"
|
||||||
|
echo "or set DOLPHIN_BIN to its path."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v xvfb-run >/dev/null 2>&1; then
|
||||||
|
echo "xvfb-run not found. Install it (e.g. 'apt install xvfb') -- Dolphin's"
|
||||||
|
echo "Qt frontend needs a display even in batch mode."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
./scripts/build-wii-iso.sh
|
||||||
|
|
||||||
|
DUSK_TEST_DOLPHIN_SECONDS="${DUSK_TEST_DOLPHIN_SECONDS:-20}"
|
||||||
|
ISO="build-wii-iso/Dusk-NTSC-U.iso"
|
||||||
|
|
||||||
|
echo "Booting $ISO in Dolphin for up to ${DUSK_TEST_DOLPHIN_SECONDS}s (batch mode, headless via Xvfb)..."
|
||||||
|
|
||||||
|
# Dolphin's batch mode (-b -e) runs the disc until told to stop -- there's
|
||||||
|
# no "the game exited cleanly" signal to wait for, so we bound it with
|
||||||
|
# timeout and treat "still running when the clock ran out" as success.
|
||||||
|
set +e
|
||||||
|
xvfb-run -a timeout "${DUSK_TEST_DOLPHIN_SECONDS}" "$DOLPHIN_BIN" -b -e "$ISO"
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# timeout returns 124 when it had to kill Dolphin after the duration
|
||||||
|
# elapsed -- that means the disc booted and kept running, the expected
|
||||||
|
# smoke-test-passed outcome, not a failure. Any other non-zero status
|
||||||
|
# means Dolphin crashed or refused to boot the disc.
|
||||||
|
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
|
||||||
|
echo "Dolphin exited with unexpected status $status -- treating as a failure."
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Wii smoke test passed (Dolphin ran $ISO for ${DUSK_TEST_DOLPHIN_SECONDS}s without crashing)."
|
||||||
@@ -39,3 +39,16 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
|||||||
sprite.uvMax[1] = uv[3];
|
sprite.uvMax[1] = uv[3];
|
||||||
return sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spritebatchsprite_t spriteBatchSpriteTranslate(
|
||||||
|
const spritebatchsprite_t *sprite,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
) {
|
||||||
|
spritebatchsprite_t out = *sprite;
|
||||||
|
out.min[0] += x;
|
||||||
|
out.min[1] += y;
|
||||||
|
out.max[0] += x;
|
||||||
|
out.max[1] += y;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,3 +38,19 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
|||||||
const float_t width,
|
const float_t width,
|
||||||
const float_t height
|
const float_t height
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of sprite translated by (x, y). Used to reposition a
|
||||||
|
* sprite cached relative to origin (0,0) at draw time, instead of
|
||||||
|
* re-deriving its geometry from scratch every frame.
|
||||||
|
*
|
||||||
|
* @param sprite The cached sprite, relative to origin (0,0).
|
||||||
|
* @param x X offset to translate by.
|
||||||
|
* @param y Y offset to translate by.
|
||||||
|
* @returns The translated sprite.
|
||||||
|
*/
|
||||||
|
spritebatchsprite_t spriteBatchSpriteTranslate(
|
||||||
|
const spritebatchsprite_t *sprite,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "script/scriptmanager.h"
|
#include "script/scriptmanager.h"
|
||||||
|
#include "script/module/scene/modulescene.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
@@ -74,7 +75,7 @@ errorret_t engineUpdate(void) {
|
|||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
|
|
||||||
errorChain(gameUpdate());
|
errorChain(gameUpdate());
|
||||||
errorChain(scriptManagerCallGlobal("update"));
|
errorChain(moduleSceneUpdateCurrent());
|
||||||
errorChain(sceneUpdate());
|
errorChain(sceneUpdate());
|
||||||
errorChain(assetUpdate());
|
errorChain(assetUpdate());
|
||||||
errorChain(uiUpdate());
|
errorChain(uiUpdate());
|
||||||
|
|||||||
@@ -7,11 +7,17 @@
|
|||||||
|
|
||||||
#include "modulescene.h"
|
#include "modulescene.h"
|
||||||
#include "script/module/modulebase.h"
|
#include "script/module/modulebase.h"
|
||||||
|
#include "script/scriptmanager.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
|
||||||
scriptproto_t MODULE_SCENE_PROTO;
|
scriptproto_t MODULE_SCENE_PROTO;
|
||||||
|
|
||||||
|
// The module last installed via Scene.set(), and the scene it owns.
|
||||||
|
// SCENE_ID_INVALID means no module is currently installed.
|
||||||
|
static jerry_value_t MODULE_SCENE_CURRENT;
|
||||||
|
static sceneid_t MODULE_SCENE_CURRENT_ID;
|
||||||
|
|
||||||
moduleBaseFunction(moduleSceneConstructor) {
|
moduleBaseFunction(moduleSceneConstructor) {
|
||||||
modulescenehandle_t *inst = (modulescenehandle_t *)memoryAllocate(
|
modulescenehandle_t *inst = (modulescenehandle_t *)memoryAllocate(
|
||||||
sizeof(modulescenehandle_t)
|
sizeof(modulescenehandle_t)
|
||||||
@@ -49,6 +55,31 @@ moduleBaseFunction(moduleSceneGetActiveStatic) {
|
|||||||
return scriptProtoCreateValue(&MODULE_SCENE_PROTO, &h);
|
return scriptProtoCreateValue(&MODULE_SCENE_PROTO, &h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moduleBaseFunction(moduleSceneSetStatic) {
|
||||||
|
moduleBaseRequireArgs(1); moduleBaseRequireObject(0);
|
||||||
|
|
||||||
|
moduleSceneTeardownCurrent();
|
||||||
|
|
||||||
|
sceneid_t newId = sceneCreate();
|
||||||
|
sceneSetActive(newId);
|
||||||
|
MODULE_SCENE_CURRENT = jerry_value_copy(args[0]);
|
||||||
|
MODULE_SCENE_CURRENT_ID = newId;
|
||||||
|
|
||||||
|
jerry_value_t initFn = moduleBaseGetProp(MODULE_SCENE_CURRENT, "init");
|
||||||
|
if(jerry_value_is_function(initFn)) {
|
||||||
|
errorret_t ret = scriptManagerCallValue(
|
||||||
|
MODULE_SCENE_CURRENT, initFn, "Scene module", "init"
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
jerry_value_free(initFn);
|
||||||
|
return moduleBaseThrowError(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jerry_value_free(initFn);
|
||||||
|
|
||||||
|
return jerry_undefined();
|
||||||
|
}
|
||||||
|
|
||||||
moduleBaseFunction(moduleSceneToString) {
|
moduleBaseFunction(moduleSceneToString) {
|
||||||
modulescenehandle_t *inst = moduleSceneGet(callInfo);
|
modulescenehandle_t *inst = moduleSceneGet(callInfo);
|
||||||
if(!inst) return jerry_string_sz("Scene(?)");
|
if(!inst) return jerry_string_sz("Scene(?)");
|
||||||
@@ -79,9 +110,15 @@ void moduleSceneInit(void) {
|
|||||||
scriptProtoDefineStaticFunc(
|
scriptProtoDefineStaticFunc(
|
||||||
&MODULE_SCENE_PROTO, "getActive", moduleSceneGetActiveStatic
|
&MODULE_SCENE_PROTO, "getActive", moduleSceneGetActiveStatic
|
||||||
);
|
);
|
||||||
|
scriptProtoDefineStaticFunc(
|
||||||
|
&MODULE_SCENE_PROTO, "set", moduleSceneSetStatic
|
||||||
|
);
|
||||||
|
|
||||||
|
MODULE_SCENE_CURRENT_ID = SCENE_ID_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
void moduleSceneDispose(void) {
|
void moduleSceneDispose(void) {
|
||||||
|
moduleSceneTeardownCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
modulescenehandle_t *moduleSceneGet(const jerry_call_info_t *callInfo) {
|
modulescenehandle_t *moduleSceneGet(const jerry_call_info_t *callInfo) {
|
||||||
@@ -89,3 +126,37 @@ modulescenehandle_t *moduleSceneGet(const jerry_call_info_t *callInfo) {
|
|||||||
&MODULE_SCENE_PROTO, callInfo->this_value
|
&MODULE_SCENE_PROTO, callInfo->this_value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void moduleSceneTeardownCurrent(void) {
|
||||||
|
if(MODULE_SCENE_CURRENT_ID == SCENE_ID_INVALID) return;
|
||||||
|
|
||||||
|
jerry_value_t disposeFn = moduleBaseGetProp(MODULE_SCENE_CURRENT, "dispose");
|
||||||
|
if(jerry_value_is_function(disposeFn)) {
|
||||||
|
errorret_t ret = scriptManagerCallValue(
|
||||||
|
MODULE_SCENE_CURRENT, disposeFn, "Scene module", "dispose"
|
||||||
|
);
|
||||||
|
errorCatch(errorPrint(ret));
|
||||||
|
}
|
||||||
|
jerry_value_free(disposeFn);
|
||||||
|
|
||||||
|
sceneDestroy(MODULE_SCENE_CURRENT_ID);
|
||||||
|
jerry_value_free(MODULE_SCENE_CURRENT);
|
||||||
|
MODULE_SCENE_CURRENT_ID = SCENE_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t moduleSceneUpdateCurrent(void) {
|
||||||
|
if(MODULE_SCENE_CURRENT_ID == SCENE_ID_INVALID) errorOk();
|
||||||
|
|
||||||
|
jerry_value_t updateFn = moduleBaseGetProp(MODULE_SCENE_CURRENT, "update");
|
||||||
|
if(!jerry_value_is_function(updateFn)) {
|
||||||
|
jerry_value_free(updateFn);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t ret = scriptManagerCallValue(
|
||||||
|
MODULE_SCENE_CURRENT, updateFn, "Scene module", "update"
|
||||||
|
);
|
||||||
|
jerry_value_free(updateFn);
|
||||||
|
errorChain(ret);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
#include "script/scriptproto.h"
|
#include "script/scriptproto.h"
|
||||||
#include "scene/scenebase.h"
|
#include "scene/scenebase.h"
|
||||||
#include <jerryscript.h>
|
#include <jerryscript.h>
|
||||||
@@ -27,7 +28,8 @@ extern scriptproto_t MODULE_SCENE_PROTO;
|
|||||||
void moduleSceneInit(void);
|
void moduleSceneInit(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes the Scene class's script resources.
|
* Disposes the Scene class's script resources, including calling
|
||||||
|
* dispose() on and freeing whatever module Scene.set() last installed.
|
||||||
*/
|
*/
|
||||||
void moduleSceneDispose(void);
|
void moduleSceneDispose(void);
|
||||||
|
|
||||||
@@ -39,3 +41,21 @@ void moduleSceneDispose(void);
|
|||||||
* @return The wrapped handle, or NULL if this_value isn't a Scene.
|
* @return The wrapped handle, or NULL if this_value isn't a Scene.
|
||||||
*/
|
*/
|
||||||
modulescenehandle_t *moduleSceneGet(const jerry_call_info_t *callInfo);
|
modulescenehandle_t *moduleSceneGet(const jerry_call_info_t *callInfo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Tears down whatever module Scene.set() currently has
|
||||||
|
* installed, if any: calls its dispose() (errors are logged, not
|
||||||
|
* propagated), destroys the scene it owns, and frees the held
|
||||||
|
* reference. No-op if nothing is installed.
|
||||||
|
*/
|
||||||
|
void moduleSceneTeardownCurrent(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls update() on whichever module Scene.set() last installed, if
|
||||||
|
* any, and if it defines one. No-op if Scene.set() has never been
|
||||||
|
* called. Called once per frame by engineUpdate().
|
||||||
|
*
|
||||||
|
* @return The error return value. An error is thrown if update() itself
|
||||||
|
* throws, or if it returns a rejected promise.
|
||||||
|
*/
|
||||||
|
errorret_t moduleSceneUpdateCurrent(void);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "scriptproto.h"
|
#include "scriptproto.h"
|
||||||
#include "script/module/modulelist.h"
|
#include "script/module/modulelist.h"
|
||||||
|
#include "script/module/require/modulerequire.h"
|
||||||
|
|
||||||
scriptmanager_t SCRIPT_MANAGER;
|
scriptmanager_t SCRIPT_MANAGER;
|
||||||
|
|
||||||
@@ -82,8 +83,13 @@ errorret_t scriptManagerExecFile(
|
|||||||
src[size] = '\0';
|
src[size] = '\0';
|
||||||
memoryFree(buffer);
|
memoryFree(buffer);
|
||||||
|
|
||||||
|
char_t dir[ASSET_FILE_NAME_MAX];
|
||||||
|
moduleRequireDirname(fname, dir, sizeof(dir));
|
||||||
|
moduleRequireDirPush(dir);
|
||||||
|
|
||||||
errorret_t ret = scriptManagerExec(src, resultOut);
|
errorret_t ret = scriptManagerExec(src, resultOut);
|
||||||
memoryFree(src);
|
memoryFree(src);
|
||||||
|
moduleRequireDirPop();
|
||||||
errorChain(ret);
|
errorChain(ret);
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -101,13 +107,24 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
jerry_value_t result = jerry_call(fn, jerry_undefined(), NULL, 0);
|
errorret_t ret = scriptManagerCallValue(
|
||||||
|
jerry_undefined(), fn, "Global function", name
|
||||||
|
);
|
||||||
jerry_value_free(fn);
|
jerry_value_free(fn);
|
||||||
|
errorChain(ret);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t scriptManagerCallValue(
|
||||||
|
const jerry_value_t thisArg,
|
||||||
|
const jerry_value_t fn,
|
||||||
|
const char_t *context,
|
||||||
|
const char_t *name
|
||||||
|
) {
|
||||||
|
jerry_value_t result = jerry_call(fn, thisArg, NULL, 0);
|
||||||
|
|
||||||
if(jerry_value_is_exception(result)) {
|
if(jerry_value_is_exception(result)) {
|
||||||
errorret_t err = scriptManagerFormatException(
|
errorret_t err = scriptManagerFormatException(context, name, result);
|
||||||
"Global function", name, result
|
|
||||||
);
|
|
||||||
jerry_value_free(result);
|
jerry_value_free(result);
|
||||||
errorChain(err);
|
errorChain(err);
|
||||||
}
|
}
|
||||||
@@ -131,7 +148,7 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
|
|||||||
if(jerry_promise_state(result) == JERRY_PROMISE_STATE_REJECTED) {
|
if(jerry_promise_state(result) == JERRY_PROMISE_STATE_REJECTED) {
|
||||||
jerry_value_t rejectVal = jerry_promise_result(result);
|
jerry_value_t rejectVal = jerry_promise_result(result);
|
||||||
errorret_t err = scriptManagerFormatValueError(
|
errorret_t err = scriptManagerFormatValueError(
|
||||||
"Global async function", name, rejectVal
|
context, name, rejectVal
|
||||||
);
|
);
|
||||||
jerry_value_free(rejectVal);
|
jerry_value_free(rejectVal);
|
||||||
jerry_value_free(result);
|
jerry_value_free(result);
|
||||||
@@ -144,6 +161,7 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t scriptManagerDispose(void) {
|
errorret_t scriptManagerDispose(void) {
|
||||||
|
moduleListDispose();
|
||||||
scriptProtoDisposeAll();
|
scriptProtoDisposeAll();
|
||||||
|
|
||||||
for(uint8_t i = 0; i < SCRIPT_MANAGER.globalKeyCacheCount; i++) {
|
for(uint8_t i = 0; i < SCRIPT_MANAGER.globalKeyCacheCount; i++) {
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ errorret_t scriptManagerInit(void);
|
|||||||
errorret_t scriptManagerExec(const char_t *script, jerry_value_t *result);
|
errorret_t scriptManagerExec(const char_t *script, jerry_value_t *result);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a JS file in the active script context.
|
* Execute a JS file in the active script context. While the file runs,
|
||||||
|
* fname's own directory is pushed as the require() base directory, so a
|
||||||
|
* top-level entry script (e.g. "scripts/init.js") can use relative
|
||||||
|
* require('./foo.js') calls the same way a required file can.
|
||||||
*
|
*
|
||||||
* @param fname The filename of the script to execute.
|
* @param fname The filename of the script to execute.
|
||||||
* @param result Optional out-parameter for the script's return value.
|
* @param result Optional out-parameter for the script's return value.
|
||||||
@@ -81,6 +84,27 @@ errorret_t scriptManagerExecFile(
|
|||||||
*/
|
*/
|
||||||
errorret_t scriptManagerCallGlobal(const char_t *name);
|
errorret_t scriptManagerCallGlobal(const char_t *name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls an already-resolved JS function value with the given `this`,
|
||||||
|
* handling a thrown exception and draining a returned promise to
|
||||||
|
* completion (same async-await semantics as scriptManagerCallGlobal).
|
||||||
|
* Caller is responsible for checking jerry_value_is_function(fn) first
|
||||||
|
* and for freeing both thisArg and fn afterward.
|
||||||
|
*
|
||||||
|
* @param thisArg The value bound to `this` inside the call.
|
||||||
|
* @param fn The function value to call.
|
||||||
|
* @param context Short label for error messages, e.g. "Scene module".
|
||||||
|
* @param name The name of the function being called, for error messages.
|
||||||
|
* @return The error return value. An error is thrown if the JS function
|
||||||
|
* itself throws, or if its returned promise rejects.
|
||||||
|
*/
|
||||||
|
errorret_t scriptManagerCallValue(
|
||||||
|
const jerry_value_t thisArg,
|
||||||
|
const jerry_value_t fn,
|
||||||
|
const char_t *context,
|
||||||
|
const char_t *name
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose of the script manager.
|
* Dispose of the script manager.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,20 +6,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "uiconsole.h"
|
#include "uiconsole.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/shader/shaderunlit.h"
|
#include "display/shader/shaderunlit.h"
|
||||||
#include "display/mesh/mesh.h"
|
#include "display/mesh/mesh.h"
|
||||||
#include "display/mesh/quad.h"
|
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
mesh_t mesh;
|
mesh_t mesh;
|
||||||
bool_t built;
|
bool_t built;
|
||||||
meshvertex_t *vertices;
|
meshvertex_t vertices[UI_CONSOLE_CACHE_VERTEX_MAX];
|
||||||
int32_t capacity;
|
|
||||||
int32_t vertexCount;
|
int32_t vertexCount;
|
||||||
int32_t cachedScanX;
|
int32_t cachedScanX;
|
||||||
int32_t cachedScanY;
|
int32_t cachedScanY;
|
||||||
@@ -62,22 +61,12 @@ errorret_t uiConsoleRebuild(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
spriteCount <= UI_CONSOLE_CACHE_GLYPH_MAX,
|
||||||
|
"Console history exceeds fixed cache capacity"
|
||||||
|
);
|
||||||
|
|
||||||
const int32_t vertexCount = spriteCount * QUAD_VERTEX_COUNT;
|
const int32_t vertexCount = spriteCount * QUAD_VERTEX_COUNT;
|
||||||
if(vertexCount > UI_CONSOLE_CACHE.capacity) {
|
|
||||||
if(UI_CONSOLE_CACHE.built) {
|
|
||||||
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
|
|
||||||
UI_CONSOLE_CACHE.built = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(UI_CONSOLE_CACHE.vertices != NULL) {
|
|
||||||
memoryFree(UI_CONSOLE_CACHE.vertices);
|
|
||||||
}
|
|
||||||
|
|
||||||
UI_CONSOLE_CACHE.vertices = (meshvertex_t *)memoryAllocate(
|
|
||||||
sizeof(meshvertex_t) * vertexCount
|
|
||||||
);
|
|
||||||
UI_CONSOLE_CACHE.capacity = vertexCount;
|
|
||||||
}
|
|
||||||
UI_CONSOLE_CACHE.vertexCount = vertexCount;
|
UI_CONSOLE_CACHE.vertexCount = vertexCount;
|
||||||
|
|
||||||
if(vertexCount > 0) {
|
if(vertexCount > 0) {
|
||||||
@@ -124,7 +113,7 @@ errorret_t uiConsoleRebuild(void) {
|
|||||||
errorChain(meshInit(
|
errorChain(meshInit(
|
||||||
&UI_CONSOLE_CACHE.mesh,
|
&UI_CONSOLE_CACHE.mesh,
|
||||||
QUAD_PRIMITIVE_TYPE,
|
QUAD_PRIMITIVE_TYPE,
|
||||||
UI_CONSOLE_CACHE.capacity,
|
UI_CONSOLE_CACHE_VERTEX_MAX,
|
||||||
UI_CONSOLE_CACHE.vertices
|
UI_CONSOLE_CACHE.vertices
|
||||||
));
|
));
|
||||||
UI_CONSOLE_CACHE.built = true;
|
UI_CONSOLE_CACHE.built = true;
|
||||||
@@ -144,10 +133,6 @@ errorret_t uiConsoleDispose(void) {
|
|||||||
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
|
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(UI_CONSOLE_CACHE.vertices != NULL) {
|
|
||||||
memoryFree(UI_CONSOLE_CACHE.vertices);
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryZero(&UI_CONSOLE_CACHE, sizeof(uiconsolecache_t));
|
memoryZero(&UI_CONSOLE_CACHE, sizeof(uiconsolecache_t));
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,16 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "display/mesh/quad.h"
|
||||||
|
|
||||||
|
// Fixed capacity for the console's cached glyph mesh, in glyphs -- the
|
||||||
|
// mesh/vertex buffer is sized to this once and never grown/shrunk, so
|
||||||
|
// history producing more non-space characters than this asserts (see
|
||||||
|
// uiConsoleRebuild) instead of reallocating.
|
||||||
|
#define UI_CONSOLE_CACHE_GLYPH_MAX 512
|
||||||
|
#define UI_CONSOLE_CACHE_VERTEX_MAX (\
|
||||||
|
UI_CONSOLE_CACHE_GLYPH_MAX * QUAD_VERTEX_COUNT \
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the console history into the scan-safe area, drawing a mesh
|
* Renders the console history into the scan-safe area, drawing a mesh
|
||||||
@@ -22,6 +32,9 @@ errorret_t uiConsoleDraw(void);
|
|||||||
* Rebuilds the cached mesh for the console's current history and
|
* Rebuilds the cached mesh for the console's current history and
|
||||||
* scan-safe origin. Called automatically by uiConsoleDraw() whenever
|
* scan-safe origin. Called automatically by uiConsoleDraw() whenever
|
||||||
* needed; only needs calling directly to force an immediate rebuild.
|
* needed; only needs calling directly to force an immediate rebuild.
|
||||||
|
* The underlying vertex buffer is a fixed-size array (see
|
||||||
|
* UI_CONSOLE_CACHE_VERTEX_MAX) -- asserts if the history produces more
|
||||||
|
* glyphs than that capacity, rather than growing it.
|
||||||
*
|
*
|
||||||
* @return Any error that occurs.
|
* @return Any error that occurs.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ errorret_t uiFPSDraw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uiLabelSetColor(&UIFPS.fpsLabel, textColor);
|
uiLabelSetColor(&UIFPS.fpsLabel, textColor);
|
||||||
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
|
if(stringCompare(fpsText, UIFPS.fpsLabel.text) != 0) {
|
||||||
|
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
|
||||||
|
}
|
||||||
errorChain(uiLabelDraw(
|
errorChain(uiLabelDraw(
|
||||||
&UIFPS.fpsLabel, (float_t)SCREEN.scanX, (float_t)SCREEN.scanY
|
&UIFPS.fpsLabel, (float_t)SCREEN.scanX, (float_t)SCREEN.scanY
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ errorret_t uiSettingsDraw(void) {
|
|||||||
const float_t y = (float_t)SCREEN.scanY +
|
const float_t y = (float_t)SCREEN.scanY +
|
||||||
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||||
|
|
||||||
errorChain(uiFrameDraw(x, y, width, height));
|
errorChain(uiFrameDrawCached(&UI_SETTINGS.frameCache, x, y, width, height));
|
||||||
|
|
||||||
const float_t contentX = x + UI_FRAME_START_X;
|
const float_t contentX = x + UI_FRAME_START_X;
|
||||||
const float_t contentY = y + UI_FRAME_START_Y;
|
const float_t contentY = y + UI_FRAME_START_Y;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uimenu.h"
|
#include "ui/widget/uimenu.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
#include "uisettingsdata.h"
|
#include "uisettingsdata.h"
|
||||||
|
|
||||||
#define UI_SETTINGS_TAB_COUNT 4
|
#define UI_SETTINGS_TAB_COUNT 4
|
||||||
@@ -29,6 +30,7 @@ typedef struct {
|
|||||||
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
|
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
|
||||||
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
|
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
|
||||||
uisettingsdata_t data;
|
uisettingsdata_t data;
|
||||||
|
uiframecache_t frameCache;
|
||||||
} uisettings_t;
|
} uisettings_t;
|
||||||
|
|
||||||
extern uisettings_t UI_SETTINGS;
|
extern uisettings_t UI_SETTINGS;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ errorret_t uiConfirmDraw(void) {
|
|||||||
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
|
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||||
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
|
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||||
|
|
||||||
errorChain(uiFrameDraw(x, y, width, height));
|
errorChain(uiFrameDrawCached(&UI_CONFIRM.frameCache, x, y, width, height));
|
||||||
|
|
||||||
float_t contentX = x + UI_FRAME_START_X;
|
float_t contentX = x + UI_FRAME_START_X;
|
||||||
float_t contentY = y + UI_FRAME_START_Y;
|
float_t contentY = y + UI_FRAME_START_Y;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uimenu.h"
|
#include "ui/widget/uimenu.h"
|
||||||
#include "ui/widget/uilabel.h"
|
#include "ui/widget/uilabel.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
|
||||||
#define UI_CONFIRM_MIN_WIDTH 160.0f
|
#define UI_CONFIRM_MIN_WIDTH 160.0f
|
||||||
#define UI_CONFIRM_INDEX_CONFIRM 0
|
#define UI_CONFIRM_INDEX_CONFIRM 0
|
||||||
@@ -31,6 +32,7 @@ typedef struct {
|
|||||||
uiconfirmcallback_t callback;
|
uiconfirmcallback_t callback;
|
||||||
void *user;
|
void *user;
|
||||||
bool_t result;
|
bool_t result;
|
||||||
|
uiframecache_t frameCache;
|
||||||
} uiconfirm_t;
|
} uiconfirm_t;
|
||||||
|
|
||||||
extern uiconfirm_t UI_CONFIRM;
|
extern uiconfirm_t UI_CONFIRM;
|
||||||
|
|||||||
@@ -72,6 +72,17 @@ errorret_t uiFrameDraw(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
spritebatchsprite_t sprites[9];
|
spritebatchsprite_t sprites[9];
|
||||||
|
uiFrameBuildSprites(sprites, x, y, width, height);
|
||||||
|
return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiFrameBuildSprites(
|
||||||
|
spritebatchsprite_t sprites[9],
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
) {
|
||||||
float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
|
float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
|
||||||
float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
||||||
|
|
||||||
@@ -112,8 +123,37 @@ errorret_t uiFrameDraw(
|
|||||||
&UI_FRAME.tileset, 2, 2,
|
&UI_FRAME.tileset, 2, 2,
|
||||||
x + width - tileW, y + height - tileH, tileW, tileH
|
x + width - tileW, y + height - tileH, tileW, tileH
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
|
errorret_t uiFrameDrawCached(
|
||||||
|
uiframecache_t *cache,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
) {
|
||||||
|
assertNotNull(cache, "Frame cache cannot be NULL");
|
||||||
|
|
||||||
|
if(
|
||||||
|
!cache->built ||
|
||||||
|
cache->lastX != x || cache->lastY != y ||
|
||||||
|
cache->lastWidth != width || cache->lastHeight != height
|
||||||
|
) {
|
||||||
|
uiFrameBuildSprites(cache->sprites, x, y, width, height);
|
||||||
|
cache->lastX = x;
|
||||||
|
cache->lastY = y;
|
||||||
|
cache->lastWidth = width;
|
||||||
|
cache->lastHeight = height;
|
||||||
|
cache->built = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
shadermaterial_t material = {
|
||||||
|
.unlit = {
|
||||||
|
.color = COLOR_WHITE,
|
||||||
|
.texture = &UI_FRAME.texture
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return spriteBatchBuffer(cache->sprites, 9, &SHADER_UNLIT, material);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiFrameDispose(void) {
|
errorret_t uiFrameDispose(void) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "display/texture/texture.h"
|
#include "display/texture/texture.h"
|
||||||
#include "display/texture/tileset.h"
|
#include "display/texture/tileset.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
#define UI_FRAME_BORDER_WIDTH 6
|
#define UI_FRAME_BORDER_WIDTH 6
|
||||||
#define UI_FRAME_BORDER_HEIGHT 6
|
#define UI_FRAME_BORDER_HEIGHT 6
|
||||||
@@ -56,6 +57,63 @@ errorret_t uiFrameDraw(
|
|||||||
const float_t height
|
const float_t height
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the 9 sprites for a 9-slice frame at the given rect. Used
|
||||||
|
* internally by uiFrameDraw and uiFrameDrawCached -- call directly only
|
||||||
|
* if you need the raw sprites instead of buffering them.
|
||||||
|
*
|
||||||
|
* @param sprites Destination array of exactly 9 sprites.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @param width Total width of the frame.
|
||||||
|
* @param height Total height of the frame.
|
||||||
|
*/
|
||||||
|
void uiFrameBuildSprites(
|
||||||
|
spritebatchsprite_t sprites[9],
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A frame's cached 9-slice sprites, plus the rect they were built for.
|
||||||
|
* Owned by whichever widget/screen draws a frame repeatedly (dialogs,
|
||||||
|
* textboxes, settings panels) so uiFrameDrawCached can skip rebuilding
|
||||||
|
* the sprites when the rect hasn't moved since the last draw.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
spritebatchsprite_t sprites[9];
|
||||||
|
bool_t built;
|
||||||
|
float_t lastX;
|
||||||
|
float_t lastY;
|
||||||
|
float_t lastWidth;
|
||||||
|
float_t lastHeight;
|
||||||
|
} uiframecache_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a 9-slice frame using a caller-owned cache, only rebuilding the
|
||||||
|
* sprites (via uiFrameBuildSprites) when x/y/width/height differ from
|
||||||
|
* the last call -- unlike uiFrameDraw, which rebuilds unconditionally
|
||||||
|
* every time. Prefer this for frames redrawn every frame at a fixed or
|
||||||
|
* rarely-changing rect (dialogs, panels); use plain uiFrameDraw for
|
||||||
|
* genuinely one-off or per-frame-varying rects.
|
||||||
|
*
|
||||||
|
* @param cache Caller-owned cache, zero-initialized before first use.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @param width Total width of the frame.
|
||||||
|
* @param height Total height of the frame.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFrameDrawCached(
|
||||||
|
uiframecache_t *cache,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes of the global UI_FRAME, releasing its GPU texture.
|
* Disposes of the global UI_FRAME, releasing its GPU texture.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ void uiSliderInitFloat(
|
|||||||
slider->step.f = step;
|
slider->step.f = step;
|
||||||
slider->value.f = mathClamp(value, min, max);
|
slider->value.f = mathClamp(value, min, max);
|
||||||
uiSliderRebuildValueLabel(slider);
|
uiSliderRebuildValueLabel(slider);
|
||||||
|
uiSliderRebuildGeometry(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiSliderInitInt(
|
void uiSliderInitInt(
|
||||||
@@ -64,6 +65,7 @@ void uiSliderInitInt(
|
|||||||
slider->step.i = step;
|
slider->step.i = step;
|
||||||
slider->value.i = mathClamp(value, min, max);
|
slider->value.i = mathClamp(value, min, max);
|
||||||
uiSliderRebuildValueLabel(slider);
|
uiSliderRebuildValueLabel(slider);
|
||||||
|
uiSliderRebuildGeometry(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t uiSliderGetFloat(const uislider_t *slider) {
|
float_t uiSliderGetFloat(const uislider_t *slider) {
|
||||||
@@ -87,6 +89,7 @@ void uiSliderSetFloat(uislider_t *slider, const float_t value) {
|
|||||||
);
|
);
|
||||||
slider->value.f = mathClamp(value, slider->min.f, slider->max.f);
|
slider->value.f = mathClamp(value, slider->min.f, slider->max.f);
|
||||||
uiSliderRebuildValueLabel(slider);
|
uiSliderRebuildValueLabel(slider);
|
||||||
|
uiSliderRebuildGeometry(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
||||||
@@ -96,6 +99,7 @@ void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
|||||||
);
|
);
|
||||||
slider->value.i = mathClamp(value, slider->min.i, slider->max.i);
|
slider->value.i = mathClamp(value, slider->min.i, slider->max.i);
|
||||||
uiSliderRebuildValueLabel(slider);
|
uiSliderRebuildValueLabel(slider);
|
||||||
|
uiSliderRebuildGeometry(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiSliderStepUp(uislider_t *slider) {
|
void uiSliderStepUp(uislider_t *slider) {
|
||||||
@@ -158,67 +162,36 @@ errorret_t uiSliderDraw(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(slider, "Slider cannot be NULL");
|
assertNotNull(slider, "Slider cannot be NULL");
|
||||||
|
|
||||||
color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE;
|
|
||||||
|
|
||||||
errorChain(uiWidgetLabelDraw(&slider->label, x, y));
|
errorChain(uiWidgetLabelDraw(&slider->label, x, y));
|
||||||
|
|
||||||
int32_t labelW, labelH;
|
|
||||||
uiWidgetLabelGetSize(&slider->label, &labelW, &labelH);
|
|
||||||
|
|
||||||
float_t trackX = x + (float_t)labelW + UI_SLIDER_GAP;
|
|
||||||
float_t trackY = y + ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
|
|
||||||
|
|
||||||
spritebatchsprite_t trackSprite = {
|
|
||||||
.min = { trackX, trackY, 0.0f },
|
|
||||||
.max = { trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
|
||||||
.uvMin = { 0.0f, 0.0f },
|
|
||||||
.uvMax = { 1.0f, 1.0f }
|
|
||||||
};
|
|
||||||
shadermaterial_t trackMaterial = {
|
shadermaterial_t trackMaterial = {
|
||||||
.unlit = {
|
.unlit = {
|
||||||
.color = COLOR_DARK_GRAY,
|
.color = COLOR_DARK_GRAY,
|
||||||
.texture = &TEXTURE_WHITE
|
.texture = &TEXTURE_WHITE
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
errorChain(spriteBatchBuffer(&trackSprite, 1, &SHADER_UNLIT, trackMaterial));
|
spritebatchsprite_t track = spriteBatchSpriteTranslate(
|
||||||
|
&slider->cachedTrack, x, y
|
||||||
|
);
|
||||||
|
errorChain(spriteBatchBuffer(&track, 1, &SHADER_UNLIT, trackMaterial));
|
||||||
|
|
||||||
float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
|
if(slider->cachedFillVisible) {
|
||||||
if(fillWidth > 0.0f) {
|
|
||||||
spritebatchsprite_t fillSprite = {
|
|
||||||
.min = { trackX, trackY, 0.0f },
|
|
||||||
.max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
|
||||||
.uvMin = { 0.0f, 0.0f },
|
|
||||||
.uvMax = { 1.0f, 1.0f }
|
|
||||||
};
|
|
||||||
shadermaterial_t fillMaterial = {
|
shadermaterial_t fillMaterial = {
|
||||||
.unlit = {
|
.unlit = {
|
||||||
.color = color,
|
.color = slider->highlighted ? COLOR_RED : COLOR_WHITE,
|
||||||
.texture = &TEXTURE_WHITE
|
.texture = &TEXTURE_WHITE
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
errorChain(spriteBatchBuffer(&fillSprite, 1, &SHADER_UNLIT, fillMaterial));
|
spritebatchsprite_t fill = spriteBatchSpriteTranslate(
|
||||||
|
&slider->cachedFill, x, y
|
||||||
|
);
|
||||||
|
errorChain(spriteBatchBuffer(&fill, 1, &SHADER_UNLIT, fillMaterial));
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t stepCount = uiSliderGetStepCount(slider);
|
if(slider->cachedMarkerCount > 0) {
|
||||||
if(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) {
|
|
||||||
spritebatchsprite_t markers[UI_SLIDER_STEP_MARKERS_MAX];
|
spritebatchsprite_t markers[UI_SLIDER_STEP_MARKERS_MAX];
|
||||||
for(int32_t i = 0; i <= stepCount; i++) {
|
for(int32_t i = 0; i < slider->cachedMarkerCount; i++) {
|
||||||
float_t markerX = trackX +
|
markers[i] = spriteBatchSpriteTranslate(&slider->cachedMarkers[i], x, y);
|
||||||
UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
|
|
||||||
markers[i] = (spritebatchsprite_t){
|
|
||||||
.min = {
|
|
||||||
markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
|
||||||
trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
|
|
||||||
0.0f
|
|
||||||
},
|
|
||||||
.max = {
|
|
||||||
markerX + UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
|
||||||
trackY + UI_SLIDER_TRACK_HEIGHT + UI_SLIDER_STEP_MARKER_OVERHANG,
|
|
||||||
0.0f
|
|
||||||
},
|
|
||||||
.uvMin = { 0.0f, 0.0f },
|
|
||||||
.uvMax = { 1.0f, 1.0f }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shadermaterial_t markerMaterial = {
|
shadermaterial_t markerMaterial = {
|
||||||
@@ -227,13 +200,15 @@ errorret_t uiSliderDraw(
|
|||||||
.texture = &TEXTURE_WHITE
|
.texture = &TEXTURE_WHITE
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
errorChain(
|
errorChain(spriteBatchBuffer(
|
||||||
spriteBatchBuffer(markers, stepCount + 1, &SHADER_UNLIT, markerMaterial)
|
markers, slider->cachedMarkerCount, &SHADER_UNLIT, markerMaterial
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
errorChain(uiWidgetLabelDraw(
|
errorChain(uiWidgetLabelDraw(
|
||||||
&slider->valueLabel, trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y
|
&slider->valueLabel,
|
||||||
|
x + slider->cachedTrack.max[0] + UI_SLIDER_GAP,
|
||||||
|
y
|
||||||
));
|
));
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
@@ -252,3 +227,56 @@ void uiSliderRebuildValueLabel(uislider_t *slider) {
|
|||||||
}
|
}
|
||||||
uiWidgetLabelSetText(&slider->valueLabel, valueText);
|
uiWidgetLabelSetText(&slider->valueLabel, valueText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uiSliderRebuildGeometry(uislider_t *slider) {
|
||||||
|
int32_t labelW, labelH;
|
||||||
|
uiWidgetLabelGetSize(&slider->label, &labelW, &labelH);
|
||||||
|
|
||||||
|
float_t trackX = (float_t)labelW + UI_SLIDER_GAP;
|
||||||
|
float_t trackY = ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
|
||||||
|
|
||||||
|
slider->cachedTrack = (spritebatchsprite_t){
|
||||||
|
.min = { trackX, trackY, 0.0f },
|
||||||
|
.max = {
|
||||||
|
trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f
|
||||||
|
},
|
||||||
|
.uvMin = { 0.0f, 0.0f },
|
||||||
|
.uvMax = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
|
||||||
|
float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
|
||||||
|
slider->cachedFillVisible = fillWidth > 0.0f;
|
||||||
|
if(slider->cachedFillVisible) {
|
||||||
|
slider->cachedFill = (spritebatchsprite_t){
|
||||||
|
.min = { trackX, trackY, 0.0f },
|
||||||
|
.max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
||||||
|
.uvMin = { 0.0f, 0.0f },
|
||||||
|
.uvMax = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t stepCount = uiSliderGetStepCount(slider);
|
||||||
|
if(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) {
|
||||||
|
for(int32_t i = 0; i <= stepCount; i++) {
|
||||||
|
float_t markerX = trackX +
|
||||||
|
UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
|
||||||
|
slider->cachedMarkers[i] = (spritebatchsprite_t){
|
||||||
|
.min = {
|
||||||
|
markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||||
|
trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||||
|
0.0f
|
||||||
|
},
|
||||||
|
.max = {
|
||||||
|
markerX + UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||||
|
trackY + UI_SLIDER_TRACK_HEIGHT + UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||||
|
0.0f
|
||||||
|
},
|
||||||
|
.uvMin = { 0.0f, 0.0f },
|
||||||
|
.uvMax = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
slider->cachedMarkerCount = stepCount + 1;
|
||||||
|
} else {
|
||||||
|
slider->cachedMarkerCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uiwidgetlabel.h"
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
#define UI_SLIDER_TRACK_WIDTH 80.0f
|
#define UI_SLIDER_TRACK_WIDTH 80.0f
|
||||||
#define UI_SLIDER_TRACK_HEIGHT 4.0f
|
#define UI_SLIDER_TRACK_HEIGHT 4.0f
|
||||||
@@ -42,6 +43,16 @@ typedef struct {
|
|||||||
uislidervalue_t max;
|
uislidervalue_t max;
|
||||||
uislidervalue_t step;
|
uislidervalue_t step;
|
||||||
bool_t highlighted;
|
bool_t highlighted;
|
||||||
|
|
||||||
|
// Track/fill/marker geometry, cached relative to origin (0,0) and only
|
||||||
|
// rebuilt when the value or step configuration changes (see
|
||||||
|
// uiSliderRebuildGeometry) -- uiSliderDraw just translates these into
|
||||||
|
// position instead of re-deriving them every frame.
|
||||||
|
spritebatchsprite_t cachedTrack;
|
||||||
|
spritebatchsprite_t cachedFill;
|
||||||
|
bool_t cachedFillVisible;
|
||||||
|
spritebatchsprite_t cachedMarkers[UI_SLIDER_STEP_MARKERS_MAX];
|
||||||
|
int32_t cachedMarkerCount;
|
||||||
} uislider_t;
|
} uislider_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,3 +203,13 @@ errorret_t uiSliderDraw(
|
|||||||
* @param slider The slider to update.
|
* @param slider The slider to update.
|
||||||
*/
|
*/
|
||||||
void uiSliderRebuildValueLabel(uislider_t *slider);
|
void uiSliderRebuildValueLabel(uislider_t *slider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the slider's cached track/fill/marker geometry (relative to
|
||||||
|
* origin (0,0)) from its current label size, value, and step
|
||||||
|
* configuration. Called internally on init and whenever the value
|
||||||
|
* changes -- uiSliderDraw never recomputes this itself.
|
||||||
|
*
|
||||||
|
* @param slider The slider to update.
|
||||||
|
*/
|
||||||
|
void uiSliderRebuildGeometry(uislider_t *slider);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ void uiTabInit(uitab_t *tab, const char_t *label) {
|
|||||||
uiWidgetLabelInit(&tab->label, &FONT_DEFAULT);
|
uiWidgetLabelInit(&tab->label, &FONT_DEFAULT);
|
||||||
uiWidgetLabelSetText(&tab->label, label);
|
uiWidgetLabelSetText(&tab->label, label);
|
||||||
tab->active = false;
|
tab->active = false;
|
||||||
|
uiTabRebuildBackground(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t uiTabIsActive(const uitab_t *tab) {
|
bool_t uiTabIsActive(const uitab_t *tab) {
|
||||||
@@ -38,15 +39,9 @@ errorret_t uiTabDraw(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(tab, "Tab cannot be NULL");
|
assertNotNull(tab, "Tab cannot be NULL");
|
||||||
|
|
||||||
int32_t labelW, labelH;
|
spritebatchsprite_t sprite = spriteBatchSpriteTranslate(
|
||||||
uiWidgetLabelGetSize(&tab->label, &labelW, &labelH);
|
&tab->cachedBackground, x, y
|
||||||
|
);
|
||||||
spritebatchsprite_t sprite = {
|
|
||||||
.min = { x, y, 0.0f },
|
|
||||||
.max = { x + (float_t)labelW, y + (float_t)labelH, 0.0f },
|
|
||||||
.uvMin = { 0.0f, 0.0f },
|
|
||||||
.uvMax = { 1.0f, 1.0f }
|
|
||||||
};
|
|
||||||
shadermaterial_t material = {
|
shadermaterial_t material = {
|
||||||
.unlit = {
|
.unlit = {
|
||||||
.color = tab->active ? COLOR_GREEN : COLOR_RED,
|
.color = tab->active ? COLOR_GREEN : COLOR_RED,
|
||||||
@@ -59,3 +54,16 @@ errorret_t uiTabDraw(
|
|||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uiTabRebuildBackground(uitab_t *tab) {
|
||||||
|
assertNotNull(tab, "Tab cannot be NULL");
|
||||||
|
|
||||||
|
int32_t labelW, labelH;
|
||||||
|
uiWidgetLabelGetSize(&tab->label, &labelW, &labelH);
|
||||||
|
tab->cachedBackground = (spritebatchsprite_t){
|
||||||
|
.min = { 0.0f, 0.0f, 0.0f },
|
||||||
|
.max = { (float_t)labelW, (float_t)labelH, 0.0f },
|
||||||
|
.uvMin = { 0.0f, 0.0f },
|
||||||
|
.uvMax = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,10 +8,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uiwidgetlabel.h"
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uiwidgetlabel_t label;
|
uiwidgetlabel_t label;
|
||||||
bool_t active;
|
bool_t active;
|
||||||
|
|
||||||
|
// Background quad, cached relative to origin (0,0) from the label's
|
||||||
|
// size at init -- uiTabDraw only translates this into position, since
|
||||||
|
// active/inactive only changes the material color, not the geometry.
|
||||||
|
spritebatchsprite_t cachedBackground;
|
||||||
} uitab_t;
|
} uitab_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,3 +58,13 @@ errorret_t uiTabDraw(
|
|||||||
const float_t x,
|
const float_t x,
|
||||||
const float_t y
|
const float_t y
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the tab's cached background quad (relative to origin (0,0))
|
||||||
|
* from its current label size. Called internally by uiTabInit -- the
|
||||||
|
* background's geometry never changes afterwards, since the tab has no
|
||||||
|
* API to change its label text.
|
||||||
|
*
|
||||||
|
* @param tab The tab to update.
|
||||||
|
*/
|
||||||
|
void uiTabRebuildBackground(uitab_t *tab);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "script/scriptmanager.h"
|
#include "script/scriptmanager.h"
|
||||||
|
|
||||||
errorret_t gameInit(void) {
|
errorret_t gameInit(void) {
|
||||||
errorChain(scriptManagerExecFile("scripts/overworldscene.js", NULL));
|
errorChain(scriptManagerExecFile("scripts/init.js", NULL));
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ void uiTextboxInit(
|
|||||||
box->maxLength = maxLength;
|
box->maxLength = maxLength;
|
||||||
box->lines = lines;
|
box->lines = lines;
|
||||||
box->linesMax = linesMax;
|
box->linesMax = linesMax;
|
||||||
|
box->glyphsBuiltForPage = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
||||||
@@ -43,6 +44,7 @@ void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
|||||||
box->scroll = 0;
|
box->scroll = 0;
|
||||||
box->layoutWidth = 0.0f;
|
box->layoutWidth = 0.0f;
|
||||||
box->layoutHeight = 0.0f;
|
box->layoutHeight = 0.0f;
|
||||||
|
box->glyphsBuiltForPage = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiTextboxBuildLayout(
|
void uiTextboxBuildLayout(
|
||||||
@@ -52,6 +54,7 @@ void uiTextboxBuildLayout(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(box, "Textbox cannot be NULL");
|
assertNotNull(box, "Textbox cannot be NULL");
|
||||||
|
|
||||||
|
box->glyphsBuiltForPage = -1;
|
||||||
box->layoutWidth = width;
|
box->layoutWidth = width;
|
||||||
box->layoutHeight = height;
|
box->layoutHeight = height;
|
||||||
box->lineCount = 0;
|
box->lineCount = 0;
|
||||||
@@ -169,12 +172,30 @@ errorret_t uiTextboxDraw(
|
|||||||
uiTextboxBuildLayout(box, contentW, contentH);
|
uiTextboxBuildLayout(box, contentW, contentH);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorChain(uiFrameDraw(x, y, width, height));
|
errorChain(uiFrameDrawCached(&box->frameCache, x, y, width, height));
|
||||||
|
|
||||||
if(box->lineCount == 0 || box->text[0] == '\0') errorOk();
|
if(box->lineCount == 0 || box->text[0] == '\0') errorOk();
|
||||||
|
|
||||||
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
if(box->glyphsBuiltForPage != box->currentPage) {
|
||||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
uiTextboxBuildPageGlyphs(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(box->glyphCount == 0) errorOk();
|
||||||
|
|
||||||
|
int32_t visibleCount = 0;
|
||||||
|
while(
|
||||||
|
visibleCount < box->glyphCount &&
|
||||||
|
box->glyphs[visibleCount].revealAt <= box->scroll
|
||||||
|
) visibleCount++;
|
||||||
|
|
||||||
|
if(visibleCount == 0) errorOk();
|
||||||
|
|
||||||
|
spritebatchsprite_t scratch[UI_TEXTBOX_PAGE_GLYPHS_MAX];
|
||||||
|
for(int32_t i = 0; i < visibleCount; i++) {
|
||||||
|
scratch[i] = spriteBatchSpriteTranslate(
|
||||||
|
&box->glyphs[i].sprite, contentX, contentY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
shadermaterial_t material = {
|
shadermaterial_t material = {
|
||||||
.unlit = {
|
.unlit = {
|
||||||
@@ -182,34 +203,47 @@ errorret_t uiTextboxDraw(
|
|||||||
.texture = FONT_DEFAULT.texture
|
.texture = FONT_DEFAULT.texture
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
errorChain(spriteBatchBuffer(scratch, visibleCount, &SHADER_UNLIT, material));
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiTextboxBuildPageGlyphs(uitextbox_t *box) {
|
||||||
|
assertNotNull(box, "Textbox cannot be NULL");
|
||||||
|
|
||||||
|
box->glyphCount = 0;
|
||||||
|
|
||||||
int32_t pageFirst = box->currentPage * box->linesPerPage;
|
int32_t pageFirst = box->currentPage * box->linesPerPage;
|
||||||
int32_t pageLast = pageFirst + box->linesPerPage;
|
int32_t pageLast = pageFirst + box->linesPerPage;
|
||||||
if(pageLast > box->lineCount) pageLast = box->lineCount;
|
if(pageLast > box->lineCount) pageLast = box->lineCount;
|
||||||
|
|
||||||
int32_t charsLeft = box->scroll;
|
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||||
|
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
|
||||||
for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) {
|
int32_t consumed = 0;
|
||||||
|
for(int32_t li = pageFirst; li < pageLast; li++) {
|
||||||
uitextboxline_t *line = &box->lines[li];
|
uitextboxline_t *line = &box->lines[li];
|
||||||
int32_t visible = line->count < charsLeft ? line->count : charsLeft;
|
float_t lineY =
|
||||||
float_t lineY = contentY +
|
|
||||||
(float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING);
|
(float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING);
|
||||||
|
|
||||||
for(int32_t ci = 0; ci < visible; ci++) {
|
for(int32_t ci = 0; ci < line->count; ci++) {
|
||||||
|
consumed++;
|
||||||
char_t c = box->text[line->start + ci];
|
char_t c = box->text[line->start + ci];
|
||||||
if(c == ' ') continue;
|
if(c == ' ') continue;
|
||||||
spritebatchsprite_t sprite = textGetSprite(
|
|
||||||
(vec2){ contentX + (float_t)ci * fontW, lineY },
|
|
||||||
c,
|
|
||||||
&FONT_DEFAULT
|
|
||||||
);
|
|
||||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
|
||||||
}
|
|
||||||
|
|
||||||
charsLeft -= visible;
|
assertTrue(
|
||||||
|
box->glyphCount < UI_TEXTBOX_PAGE_GLYPHS_MAX,
|
||||||
|
"Textbox page produces too many glyphs"
|
||||||
|
);
|
||||||
|
uitextboxglyph_t *glyph = &box->glyphs[box->glyphCount++];
|
||||||
|
glyph->sprite = textGetSprite(
|
||||||
|
(vec2){ (float_t)ci * fontW, lineY }, c, &FONT_DEFAULT
|
||||||
|
);
|
||||||
|
glyph->revealAt = consumed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errorOk();
|
box->glyphsBuiltForPage = box->currentPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) {
|
int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) {
|
||||||
|
|||||||
@@ -7,16 +7,34 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
||||||
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
||||||
#define UI_TEXTBOX_LINE_SPACING 0.0f
|
#define UI_TEXTBOX_LINE_SPACING 0.0f
|
||||||
|
|
||||||
|
// Fixed capacity for a page's cached glyph sprites (see uitextboxglyph_t).
|
||||||
|
// Sized generously above UI_TEXTBOX_LINES_PER_PAGE_MAX worth of glyphs at
|
||||||
|
// typical textbox widths; uiTextboxBuildPageGlyphs asserts if a page
|
||||||
|
// somehow produces more than this.
|
||||||
|
#define UI_TEXTBOX_PAGE_GLYPHS_MAX 512
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int32_t start;
|
int32_t start;
|
||||||
int32_t count;
|
int32_t count;
|
||||||
} uitextboxline_t;
|
} uitextboxline_t;
|
||||||
|
|
||||||
|
// A single visible glyph's cached sprite (relative to the textbox's
|
||||||
|
// content origin, i.e. (0,0)) plus the scroll value at/after which it
|
||||||
|
// becomes visible -- lets uiTextboxDraw turn the typewriter scroll into
|
||||||
|
// a simple prefix-count instead of recomputing glyph geometry every
|
||||||
|
// frame.
|
||||||
|
typedef struct {
|
||||||
|
spritebatchsprite_t sprite;
|
||||||
|
int32_t revealAt;
|
||||||
|
} uitextboxglyph_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t *text;
|
char_t *text;
|
||||||
uint32_t maxLength;
|
uint32_t maxLength;
|
||||||
@@ -34,6 +52,15 @@ typedef struct {
|
|||||||
|
|
||||||
int32_t currentPage;
|
int32_t currentPage;
|
||||||
int32_t scroll;
|
int32_t scroll;
|
||||||
|
|
||||||
|
// Cached glyph sprites for the current page (see uitextboxglyph_t),
|
||||||
|
// rebuilt only when currentPage no longer matches
|
||||||
|
// glyphsBuiltForPage -- not every frame/scroll tick.
|
||||||
|
uitextboxglyph_t glyphs[UI_TEXTBOX_PAGE_GLYPHS_MAX];
|
||||||
|
int32_t glyphCount;
|
||||||
|
int32_t glyphsBuiltForPage;
|
||||||
|
|
||||||
|
uiframecache_t frameCache;
|
||||||
} uitextbox_t;
|
} uitextbox_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,3 +163,13 @@ bool_t uiTextboxHasNextPage(const uitextbox_t *box);
|
|||||||
* @param box The textbox to advance.
|
* @param box The textbox to advance.
|
||||||
*/
|
*/
|
||||||
void uiTextboxNextPage(uitextbox_t *box);
|
void uiTextboxNextPage(uitextbox_t *box);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the cached glyph sprites (see uitextboxglyph_t) for the
|
||||||
|
* current page from its line layout. Called automatically by
|
||||||
|
* uiTextboxDraw whenever currentPage no longer matches the page the
|
||||||
|
* cache was last built for.
|
||||||
|
*
|
||||||
|
* @param box The textbox to rebuild.
|
||||||
|
*/
|
||||||
|
void uiTextboxBuildPageGlyphs(uitextbox_t *box);
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ add_subdirectory(scene)
|
|||||||
# add_subdirectory(item)
|
# add_subdirectory(item)
|
||||||
add_subdirectory(script)
|
add_subdirectory(script)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
|
add_subdirectory(ui)
|
||||||
add_subdirectory(util)
|
add_subdirectory(util)
|
||||||
@@ -7,3 +7,4 @@ include(dusktest)
|
|||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
dusktest(test_color.c)
|
dusktest(test_color.c)
|
||||||
|
dusktest(test_spritebatchsprite.c)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
|
static void test_spriteBatchSpriteTilesetPosition_create(void **state) {
|
||||||
|
tileset_t tileset = {
|
||||||
|
.tileWidth = 8,
|
||||||
|
.tileHeight = 8,
|
||||||
|
.tileCount = 4,
|
||||||
|
.columns = 2,
|
||||||
|
.rows = 2,
|
||||||
|
.uv = { 0.5f, 0.5f }
|
||||||
|
};
|
||||||
|
|
||||||
|
spritebatchsprite_t sprite = spriteBatchSpriteTilesetPosition(
|
||||||
|
&tileset, 1, 1, 10.0f, 20.0f, 8.0f, 8.0f
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_float_equal(sprite.min[0], 10.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.min[1], 20.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.min[2], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.max[0], 18.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.max[1], 28.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.uvMin[0], 0.5f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.uvMin[1], 0.5f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.uvMax[0], 1.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.uvMax[1], 1.0f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_spriteBatchSpriteTilesetPosition_zeroSize(void **state) {
|
||||||
|
tileset_t tileset = {
|
||||||
|
.tileWidth = 8,
|
||||||
|
.tileHeight = 8,
|
||||||
|
.tileCount = 1,
|
||||||
|
.columns = 1,
|
||||||
|
.rows = 1,
|
||||||
|
.uv = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
|
||||||
|
spritebatchsprite_t sprite = spriteBatchSpriteTilesetPosition(
|
||||||
|
&tileset, 0, 0, 10.0f, 20.0f, 0.0f, 8.0f
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_float_equal(sprite.min[0], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.min[1], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.max[0], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.max[1], 0.0f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_spriteBatchSpriteTranslate(void **state) {
|
||||||
|
spritebatchsprite_t sprite = {
|
||||||
|
.min = { 1.0f, 2.0f, 0.0f },
|
||||||
|
.max = { 5.0f, 6.0f, 0.0f },
|
||||||
|
.uvMin = { 0.25f, 0.5f },
|
||||||
|
.uvMax = { 0.75f, 1.0f }
|
||||||
|
};
|
||||||
|
|
||||||
|
spritebatchsprite_t translated = spriteBatchSpriteTranslate(
|
||||||
|
&sprite, 10.0f, -3.0f
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_float_equal(translated.min[0], 11.0f, 0.0001f);
|
||||||
|
assert_float_equal(translated.min[1], -1.0f, 0.0001f);
|
||||||
|
assert_float_equal(translated.max[0], 15.0f, 0.0001f);
|
||||||
|
assert_float_equal(translated.max[1], 3.0f, 0.0001f);
|
||||||
|
|
||||||
|
// UVs must be untouched by translation.
|
||||||
|
assert_float_equal(translated.uvMin[0], 0.25f, 0.0001f);
|
||||||
|
assert_float_equal(translated.uvMin[1], 0.5f, 0.0001f);
|
||||||
|
assert_float_equal(translated.uvMax[0], 0.75f, 0.0001f);
|
||||||
|
assert_float_equal(translated.uvMax[1], 1.0f, 0.0001f);
|
||||||
|
|
||||||
|
// Original sprite must be untouched.
|
||||||
|
assert_float_equal(sprite.min[0], 1.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprite.min[1], 2.0f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_spriteBatchSpriteTilesetPosition_create),
|
||||||
|
cmocka_unit_test(test_spriteBatchSpriteTilesetPosition_zeroSize),
|
||||||
|
cmocka_unit_test(test_spriteBatchSpriteTranslate),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
@@ -17,20 +17,18 @@
|
|||||||
#include "display/mesh/plane.h"
|
#include "display/mesh/plane.h"
|
||||||
#include "display/mesh/capsule.h"
|
#include "display/mesh/capsule.h"
|
||||||
#include "script/scriptmanager.h"
|
#include "script/scriptmanager.h"
|
||||||
|
#include "script/module/scene/modulescene.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#ifndef DUSK_ASSETS_DIR
|
#ifndef DUSK_ASSETS_DIR
|
||||||
#error "DUSK_ASSETS_DIR must be defined"
|
#error "DUSK_ASSETS_DIR must be defined"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Reads the real, shipped overworldscene.js from disk (not a copy embedded
|
static char_t *readFile(const char_t *relativePath) {
|
||||||
// in this test) so this test actually verifies what ships.
|
|
||||||
static char_t *readScriptSource(void) {
|
|
||||||
char_t path[512];
|
char_t path[512];
|
||||||
snprintf(
|
snprintf(path, sizeof(path), "%s/%s", DUSK_ASSETS_DIR, relativePath);
|
||||||
path, sizeof(path), "%s/scripts/overworldscene.js", DUSK_ASSETS_DIR
|
|
||||||
);
|
|
||||||
|
|
||||||
FILE *f = fopen(path, "rb");
|
FILE *f = fopen(path, "rb");
|
||||||
assert_non_null(f);
|
assert_non_null(f);
|
||||||
@@ -47,6 +45,26 @@ static char_t *readScriptSource(void) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runs the real, shipped overworldscene.js through Scene.set() the same
|
||||||
|
// way require()/init.js would -- not a copy embedded in this test, so
|
||||||
|
// this actually verifies what ships.
|
||||||
|
static errorret_t installOverworldScene(void) {
|
||||||
|
char_t *fileSrc = readFile("scripts/overworldscene.js");
|
||||||
|
|
||||||
|
const char_t *prefix = "var module = { exports: {} };\n(function(module){\n";
|
||||||
|
const char_t *suffix = "\n})(module);\nScene.set(module.exports);";
|
||||||
|
size_t wrappedLen = strlen(prefix) + strlen(fileSrc) + strlen(suffix);
|
||||||
|
char_t *wrapped = (char_t *)memoryAllocate(wrappedLen + 1);
|
||||||
|
snprintf(
|
||||||
|
wrapped, wrappedLen + 1, "%s%s%s", prefix, fileSrc, suffix
|
||||||
|
);
|
||||||
|
memoryFree(fileSrc);
|
||||||
|
|
||||||
|
errorret_t ret = scriptManagerExec(wrapped, NULL);
|
||||||
|
memoryFree(wrapped);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
static int overworld_setup(void **state) {
|
static int overworld_setup(void **state) {
|
||||||
sceneInit();
|
sceneInit();
|
||||||
|
|
||||||
@@ -71,9 +89,7 @@ static int overworld_teardown(void **state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void test_overworldscene_builds_expected_entities(void **state) {
|
static void test_overworldscene_builds_expected_entities(void **state) {
|
||||||
char_t *src = readScriptSource();
|
errorret_t ret = installOverworldScene();
|
||||||
errorret_t ret = scriptManagerExec(src, NULL);
|
|
||||||
memoryFree(src);
|
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
sceneid_t sceneId = sceneGetActive();
|
sceneid_t sceneId = sceneGetActive();
|
||||||
@@ -195,9 +211,7 @@ static void test_overworldscene_builds_expected_entities(void **state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void test_overworldscene_update_orbits_camera(void **state) {
|
static void test_overworldscene_update_orbits_camera(void **state) {
|
||||||
char_t *src = readScriptSource();
|
errorret_t ret = installOverworldScene();
|
||||||
errorret_t ret = scriptManagerExec(src, NULL);
|
|
||||||
memoryFree(src);
|
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
sceneid_t sceneId = sceneGetActive();
|
sceneid_t sceneId = sceneGetActive();
|
||||||
@@ -210,7 +224,7 @@ static void test_overworldscene_update_orbits_camera(void **state) {
|
|||||||
// Drive one frame with a known delta and confirm the camera orbited by
|
// Drive one frame with a known delta and confirm the camera orbited by
|
||||||
// exactly angle = delta * speed (0.1 * 0.5 = 0.05 rad).
|
// exactly angle = delta * speed (0.1 * 0.5 = 0.05 rad).
|
||||||
TIME.delta = 0.1f;
|
TIME.delta = 0.1f;
|
||||||
ret = scriptManagerCallGlobal("update");
|
ret = moduleSceneUpdateCurrent();
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
vec3 camPosition;
|
vec3 camPosition;
|
||||||
@@ -227,6 +241,66 @@ static void test_overworldscene_update_orbits_camera(void **state) {
|
|||||||
TIME.delta = 0.0f;
|
TIME.delta = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_scene_set_switches_and_disposes(void **state) {
|
||||||
|
errorret_t ret = installOverworldScene();
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_true(sceneGetActive() != SCENE_ID_INVALID);
|
||||||
|
|
||||||
|
// Install a second, trivial scene module in place of the first. This
|
||||||
|
// must: call the first module's dispose(), destroy its scene, then
|
||||||
|
// create+activate a new one and call the new module's init().
|
||||||
|
ret = scriptManagerExec(
|
||||||
|
"var switchState = { updateCount: 0, disposed: false };\n"
|
||||||
|
"Scene.set({\n"
|
||||||
|
" init: function() { var e = new Entity(); e.add(POSITION); },\n"
|
||||||
|
" update: function() { switchState.updateCount++; },\n"
|
||||||
|
" dispose: function() { switchState.disposed = true; }\n"
|
||||||
|
"});",
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
|
sceneid_t secondSceneId = sceneGetActive();
|
||||||
|
assert_true(secondSceneId != SCENE_ID_INVALID);
|
||||||
|
|
||||||
|
entitymanager_t *mgr = sceneGetEntities(secondSceneId);
|
||||||
|
assert_true(
|
||||||
|
entityGetComponent(mgr, 0, COMPONENT_TYPE_POSITION) !=
|
||||||
|
COMPONENT_ID_INVALID
|
||||||
|
);
|
||||||
|
// The first module's plane entity (entity 1, POSITION+PHYSICS+
|
||||||
|
// RENDERABLE) must be gone -- proves the old scene was actually torn
|
||||||
|
// down, not just deactivated. (Scene IDs are a small reused pool --
|
||||||
|
// SCENE_COUNT_MAX slots -- so secondSceneId == firstSceneId here is
|
||||||
|
// expected, not a bug: sceneCreate() picks the first free slot, and
|
||||||
|
// destroying firstSceneId frees that exact slot right back up.)
|
||||||
|
assert_true(
|
||||||
|
entityGetComponent(mgr, 1, COMPONENT_TYPE_POSITION) ==
|
||||||
|
COMPONENT_ID_INVALID
|
||||||
|
);
|
||||||
|
|
||||||
|
ret = moduleSceneUpdateCurrent();
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
|
jerry_value_t result;
|
||||||
|
ret = scriptManagerExec("switchState.updateCount", &result);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_true(jerry_value_is_number(result));
|
||||||
|
assert_int_equal((int)jerry_value_as_number(result), 1);
|
||||||
|
jerry_value_free(result);
|
||||||
|
|
||||||
|
// Switching again must call the second module's dispose().
|
||||||
|
ret = scriptManagerExec(
|
||||||
|
"Scene.set({ init: function() {} });", NULL
|
||||||
|
);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
|
ret = scriptManagerExec("switchState.disposed", &result);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_true(jerry_value_is_true(result));
|
||||||
|
jerry_value_free(result);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
assertInit();
|
assertInit();
|
||||||
const struct CMUnitTest tests[] = {
|
const struct CMUnitTest tests[] = {
|
||||||
@@ -238,6 +312,10 @@ int main(void) {
|
|||||||
test_overworldscene_update_orbits_camera,
|
test_overworldscene_update_orbits_camera,
|
||||||
overworld_setup, overworld_teardown
|
overworld_setup, overworld_teardown
|
||||||
),
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_scene_set_switches_and_disposes,
|
||||||
|
overworld_setup, overworld_teardown
|
||||||
|
),
|
||||||
};
|
};
|
||||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
include(dusktest)
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
dusktest(test_uiframe.c)
|
||||||
|
dusktest(test_uislider.c)
|
||||||
|
dusktest(test_uitab.c)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
|
||||||
|
static void test_uiFrameBuildSprites_layout(void **state) {
|
||||||
|
UI_FRAME.tileset = (tileset_t){
|
||||||
|
.tileWidth = 1,
|
||||||
|
.tileHeight = 1,
|
||||||
|
.tileCount = 9,
|
||||||
|
.columns = 3,
|
||||||
|
.rows = 3,
|
||||||
|
.uv = { 0.25f, 0.25f }
|
||||||
|
};
|
||||||
|
|
||||||
|
const float_t x = 100.0f;
|
||||||
|
const float_t y = 50.0f;
|
||||||
|
const float_t width = 40.0f;
|
||||||
|
const float_t height = 30.0f;
|
||||||
|
const float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
|
||||||
|
const float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
||||||
|
|
||||||
|
spritebatchsprite_t sprites[9];
|
||||||
|
uiFrameBuildSprites(sprites, x, y, width, height);
|
||||||
|
|
||||||
|
// Top-left corner sits exactly at the rect's origin.
|
||||||
|
assert_float_equal(sprites[0].min[0], x, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].min[1], y, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].max[0], x + tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].max[1], y + tileH, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].uvMin[0], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].uvMin[1], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].uvMax[0], 0.25f, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].uvMax[1], 0.25f, 0.0001f);
|
||||||
|
|
||||||
|
// Top-middle edge stretches to fill the width minus both corners.
|
||||||
|
assert_float_equal(sprites[1].min[0], x + tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[1].max[0], x + width - tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[1].max[1], y + tileH, 0.0001f);
|
||||||
|
|
||||||
|
// Center tile fills the remaining interior on both axes.
|
||||||
|
assert_float_equal(sprites[4].min[0], x + tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[4].min[1], y + tileH, 0.0001f);
|
||||||
|
assert_float_equal(sprites[4].max[0], x + width - tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[4].max[1], y + height - tileH, 0.0001f);
|
||||||
|
|
||||||
|
// Bottom-right corner sits exactly at the rect's far corner, sampling
|
||||||
|
// tileset column 2, row 2.
|
||||||
|
assert_float_equal(sprites[8].min[0], x + width - tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].min[1], y + height - tileH, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].max[0], x + width, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].max[1], y + height, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].uvMin[0], 0.5f, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].uvMin[1], 0.5f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_uiFrameBuildSprites_layout),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "ui/widget/uislider.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
// uiSliderInitFloat/InitInt require a real font (FONT_DEFAULT) to build
|
||||||
|
// the label's glyph cache, which needs a live GL context this test binary
|
||||||
|
// doesn't have -- see the label size caching contract for why this is
|
||||||
|
// safe to bypass here (uiSliderRebuildGeometry only reads slider->label's
|
||||||
|
// already-measured width/height, it never touches the font itself).
|
||||||
|
static void sliderSetupFloat(
|
||||||
|
uislider_t *slider,
|
||||||
|
const float_t value,
|
||||||
|
const float_t min,
|
||||||
|
const float_t max,
|
||||||
|
const float_t step
|
||||||
|
) {
|
||||||
|
memoryZero(slider, sizeof(uislider_t));
|
||||||
|
slider->label.width = 20;
|
||||||
|
slider->label.height = 10;
|
||||||
|
slider->type = UI_SLIDER_TYPE_FLOAT;
|
||||||
|
slider->min.f = min;
|
||||||
|
slider->max.f = max;
|
||||||
|
slider->step.f = step;
|
||||||
|
slider->value.f = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sliderSetupInt(
|
||||||
|
uislider_t *slider,
|
||||||
|
const int32_t value,
|
||||||
|
const int32_t min,
|
||||||
|
const int32_t max,
|
||||||
|
const int32_t step
|
||||||
|
) {
|
||||||
|
memoryZero(slider, sizeof(uislider_t));
|
||||||
|
slider->label.width = 20;
|
||||||
|
slider->label.height = 10;
|
||||||
|
slider->type = UI_SLIDER_TYPE_INT;
|
||||||
|
slider->min.i = min;
|
||||||
|
slider->max.i = max;
|
||||||
|
slider->step.i = step;
|
||||||
|
slider->value.i = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_uiSliderRebuildGeometry_trackPosition(void **state) {
|
||||||
|
uislider_t slider;
|
||||||
|
sliderSetupFloat(&slider, 0.0f, 0.0f, 1.0f, 0.1f);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
|
||||||
|
const float_t expectedX = (float_t)slider.label.width + UI_SLIDER_GAP;
|
||||||
|
const float_t expectedY =
|
||||||
|
((float_t)slider.label.height - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
|
||||||
|
|
||||||
|
assert_float_equal(slider.cachedTrack.min[0], expectedX, 0.0001f);
|
||||||
|
assert_float_equal(slider.cachedTrack.min[1], expectedY, 0.0001f);
|
||||||
|
assert_float_equal(
|
||||||
|
slider.cachedTrack.max[0], expectedX + UI_SLIDER_TRACK_WIDTH, 0.0001f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_uiSliderRebuildGeometry_fillVisibility(void **state) {
|
||||||
|
uislider_t slider;
|
||||||
|
|
||||||
|
// At the minimum value, the ratio is 0 -- no fill should be visible.
|
||||||
|
sliderSetupFloat(&slider, 0.0f, 0.0f, 10.0f, 1.0f);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
assert_false(slider.cachedFillVisible);
|
||||||
|
|
||||||
|
// Halfway to max -- fill should cover half the track.
|
||||||
|
sliderSetupFloat(&slider, 5.0f, 0.0f, 10.0f, 1.0f);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
assert_true(slider.cachedFillVisible);
|
||||||
|
assert_float_equal(
|
||||||
|
slider.cachedFill.max[0] - slider.cachedFill.min[0],
|
||||||
|
UI_SLIDER_TRACK_WIDTH * 0.5f,
|
||||||
|
0.0001f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_uiSliderRebuildGeometry_stepMarkers(void **state) {
|
||||||
|
uislider_t slider;
|
||||||
|
|
||||||
|
// 5 steps between 0 and 10 -- under the marker cap, so markers render.
|
||||||
|
sliderSetupInt(&slider, 0, 0, 10, 2);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
assert_int_equal(slider.cachedMarkerCount, 6);
|
||||||
|
assert_float_equal(
|
||||||
|
slider.cachedMarkers[0].min[0], slider.cachedTrack.min[0] -
|
||||||
|
UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||||
|
0.0001f
|
||||||
|
);
|
||||||
|
|
||||||
|
// 100 steps -- over the marker cap, so it falls back to a smooth fill
|
||||||
|
// with no markers.
|
||||||
|
sliderSetupInt(&slider, 0, 0, 100, 1);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
assert_int_equal(slider.cachedMarkerCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_uiSliderRebuildGeometry_trackPosition),
|
||||||
|
cmocka_unit_test(test_uiSliderRebuildGeometry_fillVisibility),
|
||||||
|
cmocka_unit_test(test_uiSliderRebuildGeometry_stepMarkers),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "ui/widget/uitab.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
// uiTabInit requires a real font (FONT_DEFAULT) to build the label's
|
||||||
|
// glyph cache, which needs a live GL context this test binary doesn't
|
||||||
|
// have. uiTabRebuildBackground only reads the label's already-measured
|
||||||
|
// width/height, so it's safe to set those directly and exercise it here.
|
||||||
|
static void test_uiTabRebuildBackground_matchesLabelSize(void **state) {
|
||||||
|
uitab_t tab;
|
||||||
|
memoryZero(&tab, sizeof(uitab_t));
|
||||||
|
tab.label.width = 42;
|
||||||
|
tab.label.height = 12;
|
||||||
|
|
||||||
|
uiTabRebuildBackground(&tab);
|
||||||
|
|
||||||
|
assert_float_equal(tab.cachedBackground.min[0], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(tab.cachedBackground.min[1], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(tab.cachedBackground.max[0], 42.0f, 0.0001f);
|
||||||
|
assert_float_equal(tab.cachedBackground.max[1], 12.0f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_uiTabRebuildBackground_matchesLabelSize),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
Vendored
+33
@@ -24,4 +24,37 @@ declare class Scene {
|
|||||||
|
|
||||||
/** Gets the currently active scene, or undefined if none is active. */
|
/** Gets the currently active scene, or undefined if none is active. */
|
||||||
static getActive(): Scene | undefined;
|
static getActive(): Scene | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a scene module as the current scene, the mechanism for
|
||||||
|
* switching scenes from script. Tears down whatever module a previous
|
||||||
|
* Scene.set() call installed (calling its dispose(), then destroying
|
||||||
|
* the scene it owned), then creates and activates a fresh Scene and
|
||||||
|
* calls the new module's init() with it active -- so init() can use
|
||||||
|
* `new Entity()`/etc. as usual. The module's update() (if defined) is
|
||||||
|
* then called once per engine frame until Scene.set() is called again.
|
||||||
|
*
|
||||||
|
* Typical usage (see require.d.ts):
|
||||||
|
* ```js
|
||||||
|
* var myScene = require('./myscene.js');
|
||||||
|
* Scene.set(myScene);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static set(module: SceneModule): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shape a scene module's `module.exports` should have to be usable
|
||||||
|
* with Scene.set(). All three are optional -- a module that never moves
|
||||||
|
* anything, for instance, can omit update().
|
||||||
|
*/
|
||||||
|
interface SceneModule {
|
||||||
|
/** Called once, right after Scene.set() creates and activates the scene. */
|
||||||
|
init?(): void;
|
||||||
|
|
||||||
|
/** Called once per engine frame while this module is the active scene. */
|
||||||
|
update?(): void;
|
||||||
|
|
||||||
|
/** Called once when this module is replaced by another Scene.set() call. */
|
||||||
|
dispose?(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user