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:
2026-07-31 13:21:57 -05:00
parent 07f98c119a
commit 8f8fa8f8d1
45 changed files with 1536 additions and 162 deletions
@@ -39,3 +39,16 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
sprite.uvMax[1] = uv[3];
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 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
);
+2 -1
View File
@@ -14,6 +14,7 @@
#include "scene/scene.h"
#include "asset/asset.h"
#include "script/scriptmanager.h"
#include "script/module/scene/modulescene.h"
#include "ui/ui.h"
#include "assert/assert.h"
#include "network/network.h"
@@ -74,7 +75,7 @@ errorret_t engineUpdate(void) {
consoleUpdate();
errorChain(gameUpdate());
errorChain(scriptManagerCallGlobal("update"));
errorChain(moduleSceneUpdateCurrent());
errorChain(sceneUpdate());
errorChain(assetUpdate());
errorChain(uiUpdate());
@@ -7,11 +7,17 @@
#include "modulescene.h"
#include "script/module/modulebase.h"
#include "script/scriptmanager.h"
#include "scene/scene.h"
#include "util/string.h"
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) {
modulescenehandle_t *inst = (modulescenehandle_t *)memoryAllocate(
sizeof(modulescenehandle_t)
@@ -49,6 +55,31 @@ moduleBaseFunction(moduleSceneGetActiveStatic) {
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) {
modulescenehandle_t *inst = moduleSceneGet(callInfo);
if(!inst) return jerry_string_sz("Scene(?)");
@@ -79,9 +110,15 @@ void moduleSceneInit(void) {
scriptProtoDefineStaticFunc(
&MODULE_SCENE_PROTO, "getActive", moduleSceneGetActiveStatic
);
scriptProtoDefineStaticFunc(
&MODULE_SCENE_PROTO, "set", moduleSceneSetStatic
);
MODULE_SCENE_CURRENT_ID = SCENE_ID_INVALID;
}
void moduleSceneDispose(void) {
moduleSceneTeardownCurrent();
}
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
);
}
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();
}
+21 -1
View File
@@ -6,6 +6,7 @@
*/
#pragma once
#include "error/error.h"
#include "script/scriptproto.h"
#include "scene/scenebase.h"
#include <jerryscript.h>
@@ -27,7 +28,8 @@ extern scriptproto_t MODULE_SCENE_PROTO;
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);
@@ -39,3 +41,21 @@ void moduleSceneDispose(void);
* @return The wrapped handle, or NULL if this_value isn't a Scene.
*/
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);
+23 -5
View File
@@ -13,6 +13,7 @@
#include "util/string.h"
#include "scriptproto.h"
#include "script/module/modulelist.h"
#include "script/module/require/modulerequire.h"
scriptmanager_t SCRIPT_MANAGER;
@@ -82,8 +83,13 @@ errorret_t scriptManagerExecFile(
src[size] = '\0';
memoryFree(buffer);
char_t dir[ASSET_FILE_NAME_MAX];
moduleRequireDirname(fname, dir, sizeof(dir));
moduleRequireDirPush(dir);
errorret_t ret = scriptManagerExec(src, resultOut);
memoryFree(src);
moduleRequireDirPop();
errorChain(ret);
errorOk();
}
@@ -101,13 +107,24 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
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);
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)) {
errorret_t err = scriptManagerFormatException(
"Global function", name, result
);
errorret_t err = scriptManagerFormatException(context, name, result);
jerry_value_free(result);
errorChain(err);
}
@@ -131,7 +148,7 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
if(jerry_promise_state(result) == JERRY_PROMISE_STATE_REJECTED) {
jerry_value_t rejectVal = jerry_promise_result(result);
errorret_t err = scriptManagerFormatValueError(
"Global async function", name, rejectVal
context, name, rejectVal
);
jerry_value_free(rejectVal);
jerry_value_free(result);
@@ -144,6 +161,7 @@ errorret_t scriptManagerCallGlobal(const char_t *name) {
}
errorret_t scriptManagerDispose(void) {
moduleListDispose();
scriptProtoDisposeAll();
for(uint8_t i = 0; i < SCRIPT_MANAGER.globalKeyCacheCount; i++) {
+25 -1
View File
@@ -50,7 +50,10 @@ errorret_t scriptManagerInit(void);
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 result Optional out-parameter for the script's return value.
@@ -81,6 +84,27 @@ errorret_t scriptManagerExecFile(
*/
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.
*
+8 -23
View File
@@ -6,20 +6,19 @@
*/
#include "uiconsole.h"
#include "assert/assert.h"
#include "console/console.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "display/spritebatch/spritebatch.h"
#include "display/shader/shaderunlit.h"
#include "display/mesh/mesh.h"
#include "display/mesh/quad.h"
#include "util/memory.h"
typedef struct {
mesh_t mesh;
bool_t built;
meshvertex_t *vertices;
int32_t capacity;
meshvertex_t vertices[UI_CONSOLE_CACHE_VERTEX_MAX];
int32_t vertexCount;
int32_t cachedScanX;
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;
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;
if(vertexCount > 0) {
@@ -124,7 +113,7 @@ errorret_t uiConsoleRebuild(void) {
errorChain(meshInit(
&UI_CONSOLE_CACHE.mesh,
QUAD_PRIMITIVE_TYPE,
UI_CONSOLE_CACHE.capacity,
UI_CONSOLE_CACHE_VERTEX_MAX,
UI_CONSOLE_CACHE.vertices
));
UI_CONSOLE_CACHE.built = true;
@@ -144,10 +133,6 @@ errorret_t uiConsoleDispose(void) {
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
}
if(UI_CONSOLE_CACHE.vertices != NULL) {
memoryFree(UI_CONSOLE_CACHE.vertices);
}
memoryZero(&UI_CONSOLE_CACHE, sizeof(uiconsolecache_t));
errorOk();
}
+13
View File
@@ -7,6 +7,16 @@
#pragma once
#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
@@ -22,6 +32,9 @@ errorret_t uiConsoleDraw(void);
* Rebuilds the cached mesh for the console's current history and
* scan-safe origin. Called automatically by uiConsoleDraw() whenever
* 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.
*/
+3 -1
View File
@@ -59,7 +59,9 @@ errorret_t uiFPSDraw() {
}
uiLabelSetColor(&UIFPS.fpsLabel, textColor);
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
if(stringCompare(fpsText, UIFPS.fpsLabel.text) != 0) {
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
}
errorChain(uiLabelDraw(
&UIFPS.fpsLabel, (float_t)SCREEN.scanX, (float_t)SCREEN.scanY
));
+1 -1
View File
@@ -148,7 +148,7 @@ errorret_t uiSettingsDraw(void) {
const float_t y = (float_t)SCREEN.scanY +
((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 contentY = y + UI_FRAME_START_Y;
+2
View File
@@ -8,6 +8,7 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#include "ui/frame/uiframe.h"
#include "uisettingsdata.h"
#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 applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
uisettingsdata_t data;
uiframecache_t frameCache;
} uisettings_t;
extern uisettings_t UI_SETTINGS;
+1 -1
View File
@@ -82,7 +82,7 @@ errorret_t uiConfirmDraw(void) {
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;
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 contentY = y + UI_FRAME_START_Y;
+2
View File
@@ -9,6 +9,7 @@
#include "error/error.h"
#include "ui/widget/uimenu.h"
#include "ui/widget/uilabel.h"
#include "ui/frame/uiframe.h"
#define UI_CONFIRM_MIN_WIDTH 160.0f
#define UI_CONFIRM_INDEX_CONFIRM 0
@@ -31,6 +32,7 @@ typedef struct {
uiconfirmcallback_t callback;
void *user;
bool_t result;
uiframecache_t frameCache;
} uiconfirm_t;
extern uiconfirm_t UI_CONFIRM;
+41 -1
View File
@@ -72,6 +72,17 @@ errorret_t uiFrameDraw(
}
};
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 tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
@@ -112,8 +123,37 @@ errorret_t uiFrameDraw(
&UI_FRAME.tileset, 2, 2,
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) {
+58
View File
@@ -9,6 +9,7 @@
#include "error/error.h"
#include "display/texture/texture.h"
#include "display/texture/tileset.h"
#include "display/spritebatch/spritebatchsprite.h"
#define UI_FRAME_BORDER_WIDTH 6
#define UI_FRAME_BORDER_HEIGHT 6
@@ -56,6 +57,63 @@ errorret_t uiFrameDraw(
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.
*
+76 -48
View File
@@ -39,6 +39,7 @@ void uiSliderInitFloat(
slider->step.f = step;
slider->value.f = mathClamp(value, min, max);
uiSliderRebuildValueLabel(slider);
uiSliderRebuildGeometry(slider);
}
void uiSliderInitInt(
@@ -64,6 +65,7 @@ void uiSliderInitInt(
slider->step.i = step;
slider->value.i = mathClamp(value, min, max);
uiSliderRebuildValueLabel(slider);
uiSliderRebuildGeometry(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);
uiSliderRebuildValueLabel(slider);
uiSliderRebuildGeometry(slider);
}
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);
uiSliderRebuildValueLabel(slider);
uiSliderRebuildGeometry(slider);
}
void uiSliderStepUp(uislider_t *slider) {
@@ -158,67 +162,36 @@ errorret_t uiSliderDraw(
) {
assertNotNull(slider, "Slider cannot be NULL");
color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE;
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 = {
.unlit = {
.color = COLOR_DARK_GRAY,
.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(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 }
};
if(slider->cachedFillVisible) {
shadermaterial_t fillMaterial = {
.unlit = {
.color = color,
.color = slider->highlighted ? COLOR_RED : COLOR_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(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) {
if(slider->cachedMarkerCount > 0) {
spritebatchsprite_t markers[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;
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 }
};
for(int32_t i = 0; i < slider->cachedMarkerCount; i++) {
markers[i] = spriteBatchSpriteTranslate(&slider->cachedMarkers[i], x, y);
}
shadermaterial_t markerMaterial = {
@@ -227,13 +200,15 @@ errorret_t uiSliderDraw(
.texture = &TEXTURE_WHITE
}
};
errorChain(
spriteBatchBuffer(markers, stepCount + 1, &SHADER_UNLIT, markerMaterial)
);
errorChain(spriteBatchBuffer(
markers, slider->cachedMarkerCount, &SHADER_UNLIT, markerMaterial
));
}
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();
@@ -252,3 +227,56 @@ void uiSliderRebuildValueLabel(uislider_t *slider) {
}
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;
}
}
+21
View File
@@ -8,6 +8,7 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uiwidgetlabel.h"
#include "display/spritebatch/spritebatchsprite.h"
#define UI_SLIDER_TRACK_WIDTH 80.0f
#define UI_SLIDER_TRACK_HEIGHT 4.0f
@@ -42,6 +43,16 @@ typedef struct {
uislidervalue_t max;
uislidervalue_t step;
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;
/**
@@ -192,3 +203,13 @@ errorret_t uiSliderDraw(
* @param slider The slider to update.
*/
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);
+17 -9
View File
@@ -19,6 +19,7 @@ void uiTabInit(uitab_t *tab, const char_t *label) {
uiWidgetLabelInit(&tab->label, &FONT_DEFAULT);
uiWidgetLabelSetText(&tab->label, label);
tab->active = false;
uiTabRebuildBackground(tab);
}
bool_t uiTabIsActive(const uitab_t *tab) {
@@ -38,15 +39,9 @@ errorret_t uiTabDraw(
) {
assertNotNull(tab, "Tab cannot be NULL");
int32_t labelW, labelH;
uiWidgetLabelGetSize(&tab->label, &labelW, &labelH);
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 }
};
spritebatchsprite_t sprite = spriteBatchSpriteTranslate(
&tab->cachedBackground, x, y
);
shadermaterial_t material = {
.unlit = {
.color = tab->active ? COLOR_GREEN : COLOR_RED,
@@ -59,3 +54,16 @@ errorret_t uiTabDraw(
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 }
};
}
+16
View File
@@ -8,10 +8,16 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uiwidgetlabel.h"
#include "display/spritebatch/spritebatchsprite.h"
typedef struct {
uiwidgetlabel_t label;
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;
/**
@@ -52,3 +58,13 @@ errorret_t uiTabDraw(
const float_t x,
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);
+1 -1
View File
@@ -9,7 +9,7 @@
#include "script/scriptmanager.h"
errorret_t gameInit(void) {
errorChain(scriptManagerExecFile("scripts/overworldscene.js", NULL));
errorChain(scriptManagerExecFile("scripts/init.js", NULL));
errorOk();
}
+51 -17
View File
@@ -33,6 +33,7 @@ void uiTextboxInit(
box->maxLength = maxLength;
box->lines = lines;
box->linesMax = linesMax;
box->glyphsBuiltForPage = -1;
}
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->layoutWidth = 0.0f;
box->layoutHeight = 0.0f;
box->glyphsBuiltForPage = -1;
}
void uiTextboxBuildLayout(
@@ -52,6 +54,7 @@ void uiTextboxBuildLayout(
) {
assertNotNull(box, "Textbox cannot be NULL");
box->glyphsBuiltForPage = -1;
box->layoutWidth = width;
box->layoutHeight = height;
box->lineCount = 0;
@@ -169,12 +172,30 @@ errorret_t uiTextboxDraw(
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();
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
if(box->glyphsBuiltForPage != box->currentPage) {
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 = {
.unlit = {
@@ -182,34 +203,47 @@ errorret_t uiTextboxDraw(
.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 pageLast = pageFirst + box->linesPerPage;
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];
int32_t visible = line->count < charsLeft ? line->count : charsLeft;
float_t lineY = contentY +
float_t lineY =
(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];
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) {
+37
View File
@@ -7,16 +7,34 @@
#pragma once
#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_SCROLL_CHARS_PER_TICK 1
#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 {
int32_t start;
int32_t count;
} 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 {
char_t *text;
uint32_t maxLength;
@@ -34,6 +52,15 @@ typedef struct {
int32_t currentPage;
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;
/**
@@ -136,3 +163,13 @@ bool_t uiTextboxHasNextPage(const uitextbox_t *box);
* @param box The textbox to advance.
*/
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);