From f485ec36a8f2439e99adfd8ad90c97b3a944695a Mon Sep 17 00:00:00 2001 From: Jan Racek Date: Wed, 25 Mar 2026 12:25:29 +0100 Subject: [PATCH] broken ui system --- Makefile | 3 +- src/cutscene.cpp | 96 ++++++- src/cutscene.hh | 19 +- src/luaapi.cpp | 320 +++++++++++++++++++++++- src/luaapi.hh | 31 ++- src/main.cpp | 1 + src/renderer.cpp | 7 + src/renderer.hh | 7 + src/scenemanager.cpp | 57 ++++- src/scenemanager.hh | 11 + src/splashpack.cpp | 37 ++- src/splashpack.hh | 6 + src/uisystem.cpp | 581 +++++++++++++++++++++++++++++++++++++++++++ src/uisystem.hh | 151 +++++++++++ 14 files changed, 1309 insertions(+), 18 deletions(-) create mode 100644 src/uisystem.cpp create mode 100644 src/uisystem.hh diff --git a/Makefile b/Makefile index a7654fc..dedf413 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,8 @@ src/controls.cpp \ src/profiler.cpp \ src/collision.cpp \ src/bvh.cpp \ -src/cutscene.cpp +src/cutscene.cpp \ +src/uisystem.cpp CPPFLAGS += -DPCDRV_SUPPORT=1 diff --git a/src/cutscene.cpp b/src/cutscene.cpp index bb3d9ae..45c02cd 100644 --- a/src/cutscene.cpp +++ b/src/cutscene.cpp @@ -3,6 +3,7 @@ #include #include #include +#include "uisystem.hh" namespace psxsplash { @@ -14,7 +15,8 @@ static bool cs_streq(const char* a, const char* b) { return *a == *b; } -void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio) { +void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio, + UISystem* uiSystem) { m_cutscenes = cutscenes; m_count = count; m_active = nullptr; @@ -22,6 +24,7 @@ void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioM m_nextAudio = 0; m_camera = camera; m_audio = audio; + m_uiSystem = uiSystem; } bool CutscenePlayer::play(const char* name) { @@ -68,6 +71,38 @@ bool CutscenePlayer::play(const char* name) { track.initialValues[0] = track.target->isActive() ? 1 : 0; } break; + case TrackType::UICanvasVisible: + if (m_uiSystem) { + track.initialValues[0] = m_uiSystem->isCanvasVisible(track.uiHandle) ? 1 : 0; + } + break; + case TrackType::UIElementVisible: + if (m_uiSystem) { + track.initialValues[0] = m_uiSystem->isElementVisible(track.uiHandle) ? 1 : 0; + } + break; + case TrackType::UIProgress: + if (m_uiSystem) { + track.initialValues[0] = m_uiSystem->getProgress(track.uiHandle); + } + break; + case TrackType::UIPosition: + if (m_uiSystem) { + int16_t px, py; + m_uiSystem->getPosition(track.uiHandle, px, py); + track.initialValues[0] = px; + track.initialValues[1] = py; + } + break; + case TrackType::UIColor: + if (m_uiSystem) { + uint8_t cr, cg, cb; + m_uiSystem->getColor(track.uiHandle, cr, cg, cb); + track.initialValues[0] = cr; + track.initialValues[1] = cg; + track.initialValues[2] = cb; + } + break; } } @@ -104,7 +139,7 @@ void CutscenePlayer::tick() { // Advance frame m_frame++; - if (m_frame >= m_active->totalFrames) { + if (m_frame > m_active->totalFrames) { m_active = nullptr; // Cutscene finished } } @@ -296,6 +331,63 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) { track.target->setActive(activeVal != 0); break; } + + // ── UI track types ── + + case TrackType::UICanvasVisible: { + if (!m_uiSystem) return; + CutsceneKeyframe* kf = track.keyframes; + uint8_t count = track.keyframeCount; + int16_t val = (count > 0 && m_frame < kf[0].getFrame()) + ? track.initialValues[0] : kf[0].values[0]; + for (uint8_t i = 0; i < count; i++) { + if (kf[i].getFrame() <= m_frame) val = kf[i].values[0]; + else break; + } + m_uiSystem->setCanvasVisible(track.uiHandle, val != 0); + break; + } + + case TrackType::UIElementVisible: { + if (!m_uiSystem) return; + CutsceneKeyframe* kf = track.keyframes; + uint8_t count = track.keyframeCount; + int16_t val = (count > 0 && m_frame < kf[0].getFrame()) + ? track.initialValues[0] : kf[0].values[0]; + for (uint8_t i = 0; i < count; i++) { + if (kf[i].getFrame() <= m_frame) val = kf[i].values[0]; + else break; + } + m_uiSystem->setElementVisible(track.uiHandle, val != 0); + break; + } + + case TrackType::UIProgress: { + if (!m_uiSystem) return; + lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + int16_t v = out[0]; + if (v < 0) v = 0; + if (v > 100) v = 100; + m_uiSystem->setProgress(track.uiHandle, (uint8_t)v); + break; + } + + case TrackType::UIPosition: { + if (!m_uiSystem) return; + lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + m_uiSystem->setPosition(track.uiHandle, out[0], out[1]); + break; + } + + case TrackType::UIColor: { + if (!m_uiSystem) return; + lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out); + uint8_t cr = (out[0] < 0) ? 0 : ((out[0] > 255) ? 255 : (uint8_t)out[0]); + uint8_t cg = (out[1] < 0) ? 0 : ((out[1] > 255) ? 255 : (uint8_t)out[1]); + uint8_t cb = (out[2] < 0) ? 0 : ((out[2] > 255) ? 255 : (uint8_t)out[2]); + m_uiSystem->setColor(track.uiHandle, cr, cg, cb); + break; + } } } diff --git a/src/cutscene.hh b/src/cutscene.hh index 55f905a..824f35a 100644 --- a/src/cutscene.hh +++ b/src/cutscene.hh @@ -11,6 +11,8 @@ namespace psxsplash { +class UISystem; // Forward declaration + static constexpr int MAX_CUTSCENES = 16; static constexpr int MAX_TRACKS = 8; static constexpr int MAX_KEYFRAMES = 64; @@ -22,6 +24,12 @@ enum class TrackType : uint8_t { ObjectPosition = 2, ObjectRotationY = 3, ObjectActive = 4, + // UI track types (v13+) + UICanvasVisible = 5, // Step: values[0] = 0/1. target unused, uiHandle = canvas index. + UIElementVisible= 6, // Step: values[0] = 0/1. uiHandle = element handle. + UIProgress = 7, // Linear: values[0] = 0-100. uiHandle = element handle. + UIPosition = 8, // Linear: values[0] = x, values[1] = y. uiHandle = element handle. + UIColor = 9, // Linear: values[0] = r, values[1] = g, values[2] = b. uiHandle = element handle. }; /// Per-keyframe interpolation mode. @@ -59,13 +67,16 @@ struct CutsceneTrack { uint8_t keyframeCount; uint8_t pad[2]; CutsceneKeyframe* keyframes; // Points into splashpack data (resolved at load time) - GameObject* target; // nullptr = camera track + GameObject* target; // nullptr = camera track or UI track + + /// For UI tracks: flat handle into UISystem (canvas index or element handle). + /// Set during cutscene load by resolving canvas/element names. + int16_t uiHandle; /// Initial values captured at play() time for pre-first-keyframe blending. /// For position tracks: fp12 x,y,z. For rotation tracks: raw angle values. /// For ObjectActive: values[0] = 1 (active) or 0 (inactive). int16_t initialValues[3]; - int16_t _initPad; }; struct Cutscene { @@ -82,7 +93,8 @@ struct Cutscene { class CutscenePlayer { public: /// Initialize with loaded cutscene data. Safe to pass nullptr/0 if no cutscenes. - void init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio); + void init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio, + UISystem* uiSystem = nullptr); /// Play cutscene by name. Returns false if not found. bool play(const char* name); @@ -104,6 +116,7 @@ private: uint8_t m_nextAudio = 0; Camera* m_camera = nullptr; AudioManager* m_audio = nullptr; + UISystem* m_uiSystem = nullptr; psyqo::Trig<> m_trig; diff --git a/src/luaapi.cpp b/src/luaapi.cpp index 65e4f2e..fc8ec85 100644 --- a/src/luaapi.cpp +++ b/src/luaapi.cpp @@ -4,6 +4,7 @@ #include "controls.hh" #include "camera.hh" #include "cutscene.hh" +#include "uisystem.hh" #include #include @@ -15,6 +16,7 @@ namespace psxsplash { // Static member SceneManager* LuaAPI::s_sceneManager = nullptr; CutscenePlayer* LuaAPI::s_cutscenePlayer = nullptr; +UISystem* LuaAPI::s_uiSystem = nullptr; // Scale factor: FixedPoint<12> stores 1.0 as raw 4096. // Lua scripts work in world-space units (1 = one unit), so we convert. @@ -36,9 +38,10 @@ static psyqo::Trig<> s_trig; // REGISTRATION // ============================================================================ -void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer) { +void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer, UISystem* uiSystem) { s_sceneManager = scene; s_cutscenePlayer = cutscenePlayer; + s_uiSystem = uiSystem; // ======================================================================== // ENTITY API @@ -281,6 +284,73 @@ void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cut L.setField(-2, "IsPlaying"); L.setGlobal("Cutscene"); + + // ======================================================================== + // UI API + // ======================================================================== + L.newTable(); // UI table + + L.push(UI_FindCanvas); + L.setField(-2, "FindCanvas"); + + L.push(UI_SetCanvasVisible); + L.setField(-2, "SetCanvasVisible"); + + L.push(UI_IsCanvasVisible); + L.setField(-2, "IsCanvasVisible"); + + L.push(UI_FindElement); + L.setField(-2, "FindElement"); + + L.push(UI_SetVisible); + L.setField(-2, "SetVisible"); + + L.push(UI_IsVisible); + L.setField(-2, "IsVisible"); + + L.push(UI_SetText); + L.setField(-2, "SetText"); + + L.push(UI_GetText); + L.setField(-2, "GetText"); + + L.push(UI_SetProgress); + L.setField(-2, "SetProgress"); + + L.push(UI_GetProgress); + L.setField(-2, "GetProgress"); + + L.push(UI_SetColor); + L.setField(-2, "SetColor"); + + L.push(UI_GetColor); + L.setField(-2, "GetColor"); + + L.push(UI_SetPosition); + L.setField(-2, "SetPosition"); + + L.push(UI_GetPosition); + L.setField(-2, "GetPosition"); + + L.push(UI_SetSize); + L.setField(-2, "SetSize"); + + L.push(UI_GetSize); + L.setField(-2, "GetSize"); + + L.push(UI_SetProgressColors); + L.setField(-2, "SetProgressColors"); + + L.push(UI_GetElementType); + L.setField(-2, "GetElementType"); + + L.push(UI_GetElementCount); + L.setField(-2, "GetElementCount"); + + L.push(UI_GetElementByIndex); + L.setField(-2, "GetElementByIndex"); + + L.setGlobal("UI"); } // ============================================================================ @@ -1437,4 +1507,252 @@ int LuaAPI::Cutscene_IsPlaying(lua_State* L) { return 1; } +// ============================================================================ +// UI API IMPLEMENTATION +// ============================================================================ + +int LuaAPI::UI_FindCanvas(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isString(1)) { + lua.pushNumber(-1); + return 1; + } + const char* name = lua.toString(1); + int idx = s_uiSystem->findCanvas(name); + lua.pushNumber(static_cast(idx)); + return 1; +} + +int LuaAPI::UI_SetCanvasVisible(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem) return 0; + int idx; + // Accept number (index) or string (name) + if (lua.isNumber(1)) { + idx = static_cast(lua.toNumber(1)); + } else if (lua.isString(1)) { + idx = s_uiSystem->findCanvas(lua.toString(1)); + } else { + return 0; + } + bool visible = lua.toBoolean(2); + s_uiSystem->setCanvasVisible(idx, visible); + return 0; +} + +int LuaAPI::UI_IsCanvasVisible(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem) { + lua.push(false); + return 1; + } + int idx; + if (lua.isNumber(1)) { + idx = static_cast(lua.toNumber(1)); + } else if (lua.isString(1)) { + idx = s_uiSystem->findCanvas(lua.toString(1)); + } else { + lua.push(false); + return 1; + } + lua.push(s_uiSystem->isCanvasVisible(idx)); + return 1; +} + +int LuaAPI::UI_FindElement(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1) || !lua.isString(2)) { + lua.pushNumber(-1); + return 1; + } + int canvasIdx = static_cast(lua.toNumber(1)); + const char* name = lua.toString(2); + int handle = s_uiSystem->findElement(canvasIdx, name); + lua.pushNumber(static_cast(handle)); + return 1; +} + +int LuaAPI::UI_SetVisible(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) return 0; + int handle = static_cast(lua.toNumber(1)); + bool visible = lua.toBoolean(2); + s_uiSystem->setElementVisible(handle, visible); + return 0; +} + +int LuaAPI::UI_IsVisible(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) { + lua.push(false); + return 1; + } + int handle = static_cast(lua.toNumber(1)); + lua.push(s_uiSystem->isElementVisible(handle)); + return 1; +} + +int LuaAPI::UI_SetText(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) return 0; + int handle = static_cast(lua.toNumber(1)); + const char* text = lua.isString(2) ? lua.toString(2) : ""; + s_uiSystem->setText(handle, text); + return 0; +} + +int LuaAPI::UI_SetProgress(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) return 0; + int handle = static_cast(lua.toNumber(1)); + int value = static_cast(lua.toNumber(2)); + if (value < 0) value = 0; + if (value > 100) value = 100; + s_uiSystem->setProgress(handle, (uint8_t)value); + return 0; +} + +int LuaAPI::UI_GetProgress(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) { + lua.pushNumber(0); + return 1; + } + int handle = static_cast(lua.toNumber(1)); + lua.pushNumber(static_cast(s_uiSystem->getProgress(handle))); + return 1; +} + +int LuaAPI::UI_SetColor(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) return 0; + int handle = static_cast(lua.toNumber(1)); + uint8_t r = static_cast(lua.toNumber(2)); + uint8_t g = static_cast(lua.toNumber(3)); + uint8_t b = static_cast(lua.toNumber(4)); + s_uiSystem->setColor(handle, r, g, b); + return 0; +} + +int LuaAPI::UI_SetPosition(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) return 0; + int handle = static_cast(lua.toNumber(1)); + int16_t x = static_cast(lua.toNumber(2)); + int16_t y = static_cast(lua.toNumber(3)); + s_uiSystem->setPosition(handle, x, y); + return 0; +} + +int LuaAPI::UI_GetText(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) { + lua.push(""); + return 1; + } + int handle = static_cast(lua.toNumber(1)); + lua.push(s_uiSystem->getText(handle)); + return 1; +} + +int LuaAPI::UI_GetColor(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) { + lua.pushNumber(0); lua.pushNumber(0); lua.pushNumber(0); + return 3; + } + int handle = static_cast(lua.toNumber(1)); + uint8_t r, g, b; + s_uiSystem->getColor(handle, r, g, b); + lua.pushNumber(r); lua.pushNumber(g); lua.pushNumber(b); + return 3; +} + +int LuaAPI::UI_GetPosition(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) { + lua.pushNumber(0); lua.pushNumber(0); + return 2; + } + int handle = static_cast(lua.toNumber(1)); + int16_t x, y; + s_uiSystem->getPosition(handle, x, y); + lua.pushNumber(static_cast(x)); + lua.pushNumber(static_cast(y)); + return 2; +} + +int LuaAPI::UI_SetSize(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) return 0; + int handle = static_cast(lua.toNumber(1)); + int16_t w = static_cast(lua.toNumber(2)); + int16_t h = static_cast(lua.toNumber(3)); + s_uiSystem->setSize(handle, w, h); + return 0; +} + +int LuaAPI::UI_GetSize(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) { + lua.pushNumber(0); lua.pushNumber(0); + return 2; + } + int handle = static_cast(lua.toNumber(1)); + int16_t w, h; + s_uiSystem->getSize(handle, w, h); + lua.pushNumber(static_cast(w)); + lua.pushNumber(static_cast(h)); + return 2; +} + +int LuaAPI::UI_SetProgressColors(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) return 0; + int handle = static_cast(lua.toNumber(1)); + uint8_t bgR = static_cast(lua.toNumber(2)); + uint8_t bgG = static_cast(lua.toNumber(3)); + uint8_t bgB = static_cast(lua.toNumber(4)); + uint8_t fR = static_cast(lua.toNumber(5)); + uint8_t fG = static_cast(lua.toNumber(6)); + uint8_t fB = static_cast(lua.toNumber(7)); + s_uiSystem->setProgressColors(handle, bgR, bgG, bgB, fR, fG, fB); + return 0; +} + +int LuaAPI::UI_GetElementType(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) { + lua.pushNumber(-1); + return 1; + } + int handle = static_cast(lua.toNumber(1)); + lua.pushNumber(static_cast(static_cast(s_uiSystem->getElementType(handle)))); + return 1; +} + +int LuaAPI::UI_GetElementCount(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1)) { + lua.pushNumber(0); + return 1; + } + int canvasIdx = static_cast(lua.toNumber(1)); + lua.pushNumber(static_cast(s_uiSystem->getCanvasElementCount(canvasIdx))); + return 1; +} + +int LuaAPI::UI_GetElementByIndex(lua_State* L) { + psyqo::Lua lua(L); + if (!s_uiSystem || !lua.isNumber(1) || !lua.isNumber(2)) { + lua.pushNumber(-1); + return 1; + } + int canvasIdx = static_cast(lua.toNumber(1)); + int elemIdx = static_cast(lua.toNumber(2)); + int handle = s_uiSystem->getCanvasElementHandle(canvasIdx, elemIdx); + lua.pushNumber(static_cast(handle)); + return 1; +} + } // namespace psxsplash diff --git a/src/luaapi.hh b/src/luaapi.hh index 62e12a4..0330910 100644 --- a/src/luaapi.hh +++ b/src/luaapi.hh @@ -8,6 +8,7 @@ namespace psxsplash { class SceneManager; // Forward declaration class CutscenePlayer; // Forward declaration +class UISystem; // Forward declaration /** * Lua API - Provides game scripting functionality @@ -24,7 +25,7 @@ class CutscenePlayer; // Forward declaration class LuaAPI { public: // Initialize all API modules - static void RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer = nullptr); + static void RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer = nullptr, UISystem* uiSystem = nullptr); // Called once per frame to advance the Lua frame counter static void IncrementFrameCount(); @@ -39,6 +40,9 @@ private: // Cutscene player pointer (set during RegisterAll) static CutscenePlayer* s_cutscenePlayer; + // UI system pointer (set during RegisterAll) + static UISystem* s_uiSystem; + // ======================================================================== // ENTITY API // ======================================================================== @@ -267,6 +271,31 @@ private: // Cutscene.IsPlaying() -> boolean static int Cutscene_IsPlaying(lua_State* L); + // ======================================================================== + // UI API - Canvas and element control + // ======================================================================== + + static int UI_FindCanvas(lua_State* L); + static int UI_SetCanvasVisible(lua_State* L); + static int UI_IsCanvasVisible(lua_State* L); + static int UI_FindElement(lua_State* L); + static int UI_SetVisible(lua_State* L); + static int UI_IsVisible(lua_State* L); + static int UI_SetText(lua_State* L); + static int UI_GetText(lua_State* L); + static int UI_SetProgress(lua_State* L); + static int UI_GetProgress(lua_State* L); + static int UI_SetColor(lua_State* L); + static int UI_GetColor(lua_State* L); + static int UI_SetPosition(lua_State* L); + static int UI_GetPosition(lua_State* L); + static int UI_SetSize(lua_State* L); + static int UI_GetSize(lua_State* L); + static int UI_SetProgressColors(lua_State* L); + static int UI_GetElementType(lua_State* L); + static int UI_GetElementCount(lua_State* L); + static int UI_GetElementByIndex(lua_State* L); + // ======================================================================== // HELPERS // ======================================================================== diff --git a/src/main.cpp b/src/main.cpp index 6a63cf8..f082eee 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -52,6 +52,7 @@ void PSXSplash::prepare() { void PSXSplash::createScene() { m_font.uploadSystemFont(gpu()); + psxsplash::SceneManager::SetFont(&m_font); pushScene(&mainScene); } diff --git a/src/renderer.cpp b/src/renderer.cpp index 323127d..9864ff4 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -16,6 +16,7 @@ #include #include "gtemath.hh" +#include "uisystem.hh" using namespace psyqo::fixed_point_literals; using namespace psyqo::trig_literals; @@ -275,8 +276,10 @@ void psxsplash::Renderer::Render(eastl::vector& objects) { for (int i = 0; i < obj->polyCount; i++) processTriangle(obj->polygons[i], fogFarSZ, ot, balloc); } + if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc); m_gpu.getNextClear(clear.primitive, m_clearcolor); m_gpu.chain(clear); m_gpu.chain(ot); + if (m_uiSystem) m_uiSystem->renderText(m_gpu); m_frameCount++; } @@ -307,8 +310,10 @@ void psxsplash::Renderer::RenderWithBVH(eastl::vector& objects, con } processTriangle(obj->polygons[ref.triangleIndex], fogFarSZ, ot, balloc); } + if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc); m_gpu.getNextClear(clear.primitive, m_clearcolor); m_gpu.chain(clear); m_gpu.chain(ot); + if (m_uiSystem) m_uiSystem->renderText(m_gpu); m_frameCount++; } @@ -750,8 +755,10 @@ void psxsplash::Renderer::RenderWithRooms(eastl::vector& objects, } #endif + if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc); m_gpu.getNextClear(clear.primitive, m_clearcolor); m_gpu.chain(clear); m_gpu.chain(ot); + if (m_uiSystem) m_uiSystem->renderText(m_gpu); m_frameCount++; } diff --git a/src/renderer.hh b/src/renderer.hh index 23878fd..ab220ee 100644 --- a/src/renderer.hh +++ b/src/renderer.hh @@ -20,6 +20,8 @@ namespace psxsplash { +class UISystem; // Forward declaration + struct FogConfig { bool enabled = false; psyqo::Color color = {.r = 0, .g = 0, .b = 0}; @@ -55,6 +57,9 @@ class Renderer final { void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY, int16_t width, int16_t height); + void SetUISystem(UISystem* ui) { m_uiSystem = ui; } + psyqo::GPU& getGPU() { return m_gpu; } + static Renderer& GetInstance() { psyqo::Kernel::assert(instance != nullptr, "Access to renderer was tried without prior initialization"); @@ -78,6 +83,8 @@ class Renderer final { FogConfig m_fog; psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0}; + UISystem* m_uiSystem = nullptr; + TriangleRef m_visibleRefs[MAX_VISIBLE_TRIANGLES]; int m_frameCount = 0; diff --git a/src/scenemanager.cpp b/src/scenemanager.cpp index 70855f8..d1a6c82 100644 --- a/src/scenemanager.cpp +++ b/src/scenemanager.cpp @@ -16,6 +16,9 @@ using namespace psyqo::fixed_point_literals; using namespace psxsplash; +// Static member definition +psyqo::Font<>* psxsplash::SceneManager::s_font = nullptr; + void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData) { L.Reset(); @@ -23,7 +26,7 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData) { m_audio.init(); // Register the Lua API - LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer); + LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer, &m_uiSystem); #ifdef PSXSPLASH_PROFILER debug::Profiler::getInstance().initialize(); @@ -84,9 +87,56 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData) { m_cutsceneCount > 0 ? m_cutscenes : nullptr, m_cutsceneCount, &m_currentCamera, - &m_audio + &m_audio, + &m_uiSystem ); + // Initialize UI system (v13+) + if (sceneSetup.uiCanvasCount > 0 && sceneSetup.uiTableOffset != 0 && s_font != nullptr) { + m_uiSystem.init(*s_font); + m_uiSystem.loadFromSplashpack(splashpackData, sceneSetup.uiCanvasCount, + sceneSetup.uiFontCount, sceneSetup.uiTableOffset); + m_uiSystem.uploadFonts(Renderer::GetInstance().getGPU()); + Renderer::GetInstance().SetUISystem(&m_uiSystem); + + // Resolve UI track handles: the splashpack loader stored raw name pointers + // in CutsceneTrack.target for UI tracks. Now that UISystem is loaded, resolve + // those names to canvas indices / element handles. + for (int ci = 0; ci < m_cutsceneCount; ci++) { + for (uint8_t ti = 0; ti < m_cutscenes[ci].trackCount; ti++) { + auto& track = m_cutscenes[ci].tracks[ti]; + bool isUI = static_cast(track.trackType) >= 5; + if (!isUI || track.target == nullptr) continue; + + const char* nameStr = reinterpret_cast(track.target); + track.target = nullptr; // Clear the temporary name pointer + + if (track.trackType == TrackType::UICanvasVisible) { + // Name is just the canvas name + track.uiHandle = static_cast(m_uiSystem.findCanvas(nameStr)); + } else { + // Name is "canvasName/elementName" — find the '/' separator + const char* sep = nameStr; + while (*sep && *sep != '/') sep++; + if (*sep == '/') { + // Temporarily null-terminate the canvas portion + // (nameStr points into splashpack data, which is mutable) + char* mutableSep = const_cast(sep); + *mutableSep = '\0'; + int canvasIdx = m_uiSystem.findCanvas(nameStr); + *mutableSep = '/'; // Restore the separator + if (canvasIdx >= 0) { + track.uiHandle = static_cast( + m_uiSystem.findElement(canvasIdx, sep + 1)); + } + } + } + } + } + } else { + Renderer::GetInstance().SetUISystem(nullptr); + } + m_playerPosition = sceneSetup.playerStartPosition; playerRotationX = 0.0_pi; @@ -607,6 +657,9 @@ void psxsplash::SceneManager::clearScene() { m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player // BVH, WorldCollision, and NavRegions will be overwritten by next load + // Reset UI system (disconnect from renderer before splashpack data disappears) + Renderer::GetInstance().SetUISystem(nullptr); + // Reset room/portal pointers (they point into splashpack data which is being freed) m_rooms = nullptr; m_roomCount = 0; diff --git a/src/scenemanager.hh b/src/scenemanager.hh index 027634f..1d1e38c 100644 --- a/src/scenemanager.hh +++ b/src/scenemanager.hh @@ -20,6 +20,7 @@ #include "luaapi.hh" #include "sceneloader.hh" #include "cutscene.hh" +#include "uisystem.hh" namespace psxsplash { class SceneManager { @@ -27,6 +28,10 @@ class SceneManager { void InitializeScene(uint8_t* splashpackData); void GameTick(psyqo::GPU &gpu); + // Font access (set from main.cpp after uploadSystemFont) + static void SetFont(psyqo::Font<>* font) { s_font = font; } + static psyqo::Font<>* GetFont() { return s_font; } + // Trigger event callbacks (called by CollisionSystem) void fireTriggerEnter(uint16_t triggerObjIdx, uint16_t otherObjIdx); void fireTriggerStay(uint16_t triggerObjIdx, uint16_t otherObjIdx); @@ -119,6 +124,9 @@ class SceneManager { int m_cutsceneCount = 0; CutscenePlayer m_cutscenePlayer; + // UI system (v13+) + UISystem m_uiSystem; + psxsplash::Controls m_controls; psxsplash::Camera m_currentCamera; @@ -141,6 +149,9 @@ class SceneManager { bool freecam = false; + // Static font pointer (set from main.cpp) + static psyqo::Font<>* s_font; + // Scene transition state int m_currentSceneIndex = 0; int m_pendingSceneIndex = -1; // -1 = no pending load diff --git a/src/splashpack.cpp b/src/splashpack.cpp index c0427e0..68696b3 100644 --- a/src/splashpack.cpp +++ b/src/splashpack.cpp @@ -83,8 +83,13 @@ struct SPLASHPACKFileHeader { uint16_t cutsceneCount; uint16_t reserved_cs; uint32_t cutsceneTableOffset; + // Version 13 additions (UI system + fonts): + uint16_t uiCanvasCount; + uint8_t uiFontCount; + uint8_t uiPad13; + uint32_t uiTableOffset; }; -static_assert(sizeof(SPLASHPACKFileHeader) == 104, "SPLASHPACKFileHeader must be 104 bytes"); +static_assert(sizeof(SPLASHPACKFileHeader) == 112, "SPLASHPACKFileHeader must be 112 bytes"); struct SPLASHPACKTextureAtlas { uint32_t polygonsOffset; @@ -104,7 +109,7 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null"); psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast(data); psyqo::Kernel::assert(__builtin_memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic"); - psyqo::Kernel::assert(header->version >= 12, "Splashpack version too old (need v12+): re-export from SplashEdit"); + psyqo::Kernel::assert(header->version >= 13, "Splashpack version too old (need v13+): re-export from SplashEdit"); setup.playerStartPosition = header->playerStartPos; setup.playerStartRotation = header->playerStartRot; @@ -354,15 +359,23 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup ? reinterpret_cast(data + kfOff) : nullptr; - // Resolve target object by name + // Resolve target object by name (or store UI name for later resolution) track.target = nullptr; + track.uiHandle = -1; if (objNameLen > 0 && objNameOff != 0) { const char* objName = reinterpret_cast(data + objNameOff); - for (size_t oi = 0; oi < setup.objectNames.size(); oi++) { - if (setup.objectNames[oi] && - sp_streq(setup.objectNames[oi], objName)) { - track.target = setup.objects[oi]; - break; + bool isUITrack = static_cast(track.trackType) >= 5; + if (isUITrack) { + // Store the raw name pointer temporarily in target + // (will be resolved to uiHandle later by scenemanager) + track.target = reinterpret_cast(const_cast(objName)); + } else { + for (size_t oi = 0; oi < setup.objectNames.size(); oi++) { + if (setup.objectNames[oi] && + sp_streq(setup.objectNames[oi], objName)) { + track.target = setup.objects[oi]; + break; + } } } // If not found, target stays nullptr — track will be skipped at runtime @@ -374,6 +387,7 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup cs.tracks[ti].keyframeCount = 0; cs.tracks[ti].keyframes = nullptr; cs.tracks[ti].target = nullptr; + cs.tracks[ti].uiHandle = -1; cs.tracks[ti].initialValues[0] = 0; cs.tracks[ti].initialValues[1] = 0; cs.tracks[ti].initialValues[2] = 0; @@ -383,6 +397,13 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup } } + // Read UI canvas data (version 13+) + if (header->version >= 13) { + setup.uiCanvasCount = header->uiCanvasCount; + setup.uiFontCount = header->uiFontCount; + setup.uiTableOffset = header->uiTableOffset; + } + } } // namespace psxsplash diff --git a/src/splashpack.hh b/src/splashpack.hh index dea711c..31880da 100644 --- a/src/splashpack.hh +++ b/src/splashpack.hh @@ -13,6 +13,7 @@ #include "audiomanager.hh" #include "interactable.hh" #include "cutscene.hh" +#include "uisystem.hh" namespace psxsplash { @@ -90,6 +91,11 @@ struct SplashpackSceneSetup { // Cutscenes (version 12+) Cutscene loadedCutscenes[MAX_CUTSCENES]; int cutsceneCount = 0; + + // UI system (v13+) + uint16_t uiCanvasCount = 0; + uint8_t uiFontCount = 0; + uint32_t uiTableOffset = 0; }; class SplashPackLoader { diff --git a/src/uisystem.cpp b/src/uisystem.cpp new file mode 100644 index 0000000..8b392d5 --- /dev/null +++ b/src/uisystem.cpp @@ -0,0 +1,581 @@ +#include "uisystem.hh" + +#include +#include +#include +#include +#include + +namespace psxsplash { + +// Bare-metal string compare (no libc) +static bool ui_streq(const char* a, const char* b) { + while (*a && *b) { + if (*a++ != *b++) return false; + } + return *a == *b; +} + +// ============================================================================ +// Init +// ============================================================================ + +void UISystem::init(psyqo::Font<>& systemFont) { + m_systemFont = &systemFont; + m_canvasCount = 0; + m_elementCount = 0; + m_pendingTextCount = 0; + m_fontCount = 0; +} + +// ============================================================================ +// Load from splashpack (zero-copy, pointer fixup) +// ============================================================================ + +void UISystem::loadFromSplashpack(uint8_t* data, uint16_t canvasCount, + uint8_t fontCount, uint32_t tableOffset) { + if (tableOffset == 0) return; + + uint8_t* ptr = data + tableOffset; + + // ── Parse font descriptors (16 bytes each, before canvas data) ── + if (fontCount > UI_MAX_FONTS - 1) fontCount = UI_MAX_FONTS - 1; + m_fontCount = fontCount; + for (int fi = 0; fi < fontCount; fi++) { + UIFontDesc& fd = m_fontDescs[fi]; + fd.glyphW = ptr[0]; + fd.glyphH = ptr[1]; + fd.vramX = *reinterpret_cast(ptr + 2); + fd.vramY = *reinterpret_cast(ptr + 4); + fd.textureH = *reinterpret_cast(ptr + 6); + uint32_t dataOff = *reinterpret_cast(ptr + 8); + fd.pixelDataSize = *reinterpret_cast(ptr + 12); + fd.pixelData = (dataOff != 0) ? (data + dataOff) : nullptr; + ptr += 16; + } + + // ── Parse canvas descriptors ── + if (canvasCount == 0) return; + if (canvasCount > UI_MAX_CANVASES) canvasCount = UI_MAX_CANVASES; + + // Canvas descriptor table: 12 bytes per entry + // struct { uint32_t dataOffset; uint8_t nameLen; uint8_t sortOrder; + // uint8_t elementCount; uint8_t flags; uint32_t nameOffset; } + uint8_t* tablePtr = ptr; // starts right after font descriptors + m_canvasCount = canvasCount; + m_elementCount = 0; + + for (int ci = 0; ci < canvasCount; ci++) { + uint32_t dataOffset = *reinterpret_cast(tablePtr); tablePtr += 4; + uint8_t nameLen = *tablePtr++; + uint8_t sortOrder = *tablePtr++; + uint8_t elementCount = *tablePtr++; + uint8_t flags = *tablePtr++; + uint32_t nameOffset = *reinterpret_cast(tablePtr); tablePtr += 4; + + UICanvas& cv = m_canvases[ci]; + cv.name = (nameLen > 0 && nameOffset != 0) + ? reinterpret_cast(data + nameOffset) + : ""; + cv.visible = (flags & 0x01) != 0; + cv.sortOrder = sortOrder; + cv.elements = &m_elements[m_elementCount]; + + // Cap element count against pool + if (m_elementCount + elementCount > UI_MAX_ELEMENTS) + elementCount = (uint8_t)(UI_MAX_ELEMENTS - m_elementCount); + cv.elementCount = elementCount; + + // Parse element array (48 bytes per entry) + uint8_t* elemPtr = data + dataOffset; + for (int ei = 0; ei < elementCount; ei++) { + UIElement& el = m_elements[m_elementCount++]; + + // Identity (8 bytes) + el.type = static_cast(*elemPtr++); + uint8_t eFlags = *elemPtr++; + el.visible = (eFlags & 0x01) != 0; + uint8_t eNameLen = *elemPtr++; + elemPtr++; // pad0 + uint32_t eNameOff = *reinterpret_cast(elemPtr); elemPtr += 4; + el.name = (eNameLen > 0 && eNameOff != 0) + ? reinterpret_cast(data + eNameOff) + : ""; + + // Layout (8 bytes) + el.x = *reinterpret_cast(elemPtr); elemPtr += 2; + el.y = *reinterpret_cast(elemPtr); elemPtr += 2; + el.w = *reinterpret_cast(elemPtr); elemPtr += 2; + el.h = *reinterpret_cast(elemPtr); elemPtr += 2; + + // Anchors (4 bytes) + el.anchorMinX = *elemPtr++; + el.anchorMinY = *elemPtr++; + el.anchorMaxX = *elemPtr++; + el.anchorMaxY = *elemPtr++; + + // Primary color (4 bytes) + el.colorR = *elemPtr++; + el.colorG = *elemPtr++; + el.colorB = *elemPtr++; + elemPtr++; // pad1 + + // Type-specific data (16 bytes) + uint8_t* typeData = elemPtr; + elemPtr += 16; + + // Initialize union to zero + for (int i = 0; i < (int)sizeof(UIImageData); i++) + reinterpret_cast(&el.image)[i] = 0; + + switch (el.type) { + case UIElementType::Image: + el.image.texpageX = typeData[0]; + el.image.texpageY = typeData[1]; + el.image.clutX = *reinterpret_cast(&typeData[2]); + el.image.clutY = *reinterpret_cast(&typeData[4]); + el.image.u0 = typeData[6]; + el.image.v0 = typeData[7]; + el.image.u1 = typeData[8]; + el.image.v1 = typeData[9]; + el.image.bitDepth = typeData[10]; + break; + case UIElementType::Progress: + el.progress.bgR = typeData[0]; + el.progress.bgG = typeData[1]; + el.progress.bgB = typeData[2]; + el.progress.value = typeData[3]; + break; + case UIElementType::Text: + el.textData.fontIndex = typeData[0]; // 0=system, 1+=custom + break; + default: + break; + } + + // Text content offset (8 bytes) + uint32_t textOff = *reinterpret_cast(elemPtr); elemPtr += 4; + elemPtr += 4; // pad2 + + // Initialize text buffer + el.textBuf[0] = '\0'; + if (el.type == UIElementType::Text && textOff != 0) { + const char* src = reinterpret_cast(data + textOff); + int ti = 0; + while (ti < UI_TEXT_BUF - 1 && src[ti] != '\0') { + el.textBuf[ti] = src[ti]; + ti++; + } + el.textBuf[ti] = '\0'; + } + } + } + + // Insertion sort canvases by sortOrder (ascending = back-to-front) + for (int i = 1; i < m_canvasCount; i++) { + UICanvas tmp = m_canvases[i]; + int j = i - 1; + while (j >= 0 && m_canvases[j].sortOrder > tmp.sortOrder) { + m_canvases[j + 1] = m_canvases[j]; + j--; + } + m_canvases[j + 1] = tmp; + } +} + +// ============================================================================ +// Layout resolution +// ============================================================================ + +void UISystem::resolveLayout(const UIElement& el, + int16_t& outX, int16_t& outY, + int16_t& outW, int16_t& outH) const { + // Anchor gives the origin point in screen space (8.8 fixed → pixel) + int ax = ((int)el.anchorMinX * VRAM_RES_WIDTH) >> 8; + int ay = ((int)el.anchorMinY * VRAM_RES_HEIGHT) >> 8; + outX = (int16_t)(ax + el.x); + outY = (int16_t)(ay + el.y); + + // Stretch: anchorMax != anchorMin means width/height is determined by span + offset + if (el.anchorMaxX != el.anchorMinX) { + int bx = ((int)el.anchorMaxX * VRAM_RES_WIDTH) >> 8; + outW = (int16_t)(bx - ax + el.w); + } else { + outW = el.w; + } + if (el.anchorMaxY != el.anchorMinY) { + int by = ((int)el.anchorMaxY * VRAM_RES_HEIGHT) >> 8; + outH = (int16_t)(by - ay + el.h); + } else { + outH = el.h; + } + + // Clamp to screen bounds (never draw outside the framebuffer) + if (outX < 0) { outW += outX; outX = 0; } + if (outY < 0) { outH += outY; outY = 0; } + if (outW <= 0) outW = 1; + if (outH <= 0) outH = 1; + if (outX + outW > VRAM_RES_WIDTH) outW = (int16_t)(VRAM_RES_WIDTH - outX); + if (outY + outH > VRAM_RES_HEIGHT) outH = (int16_t)(VRAM_RES_HEIGHT - outY); +} + +// ============================================================================ +// TPage construction for UI images +// ============================================================================ + +psyqo::PrimPieces::TPageAttr UISystem::makeTPage(const UIImageData& img) { + psyqo::PrimPieces::TPageAttr tpage; + tpage.setPageX(img.texpageX); + tpage.setPageY(img.texpageY); + // Color mode from bitDepth: 0→Tex4Bits, 1→Tex8Bits, 2→Tex16Bits + switch (img.bitDepth) { + case 0: + tpage.set(psyqo::Prim::TPageAttr::Tex4Bits); + break; + case 1: + tpage.set(psyqo::Prim::TPageAttr::Tex8Bits); + break; + case 2: + default: + tpage.set(psyqo::Prim::TPageAttr::Tex16Bits); + break; + } + tpage.setDithering(false); // UI doesn't need dithering + return tpage; +} + +// ============================================================================ +// Render a single element into the OT +// ============================================================================ + +void UISystem::renderElement(UIElement& el, + psyqo::OrderingTable& ot, + psyqo::BumpAllocator& balloc) { + int16_t x, y, w, h; + resolveLayout(el, x, y, w, h); + + switch (el.type) { + case UIElementType::Box: { + auto& frag = balloc.allocateFragment(); + frag.primitive.setColor(psyqo::Color{.r = el.colorR, .g = el.colorG, .b = el.colorB}); + frag.primitive.position = {.x = x, .y = y}; + frag.primitive.size = {.x = w, .y = h}; + frag.primitive.setOpaque(); + ot.insert(frag, 0); + break; + } + + case UIElementType::Progress: { + // Background: full rect + auto& bgFrag = balloc.allocateFragment(); + bgFrag.primitive.setColor(psyqo::Color{.r = el.progress.bgR, .g = el.progress.bgG, .b = el.progress.bgB}); + bgFrag.primitive.position = {.x = x, .y = y}; + bgFrag.primitive.size = {.x = w, .y = h}; + bgFrag.primitive.setOpaque(); + ot.insert(bgFrag, 1); + + // Fill: partial width + int fillW = (int)el.progress.value * w / 100; + if (fillW < 0) fillW = 0; + if (fillW > w) fillW = w; + if (fillW > 0) { + auto& fillFrag = balloc.allocateFragment(); + fillFrag.primitive.setColor(psyqo::Color{.r = el.colorR, .g = el.colorG, .b = el.colorB}); + fillFrag.primitive.position = {.x = x, .y = y}; + fillFrag.primitive.size = {.x = (int16_t)fillW, .y = h}; + fillFrag.primitive.setOpaque(); + ot.insert(fillFrag, 0); + } + break; + } + + case UIElementType::Image: { + psyqo::PrimPieces::TPageAttr tpage = makeTPage(el.image); + psyqo::PrimPieces::ClutIndex clut(el.image.clutX, el.image.clutY); + psyqo::Color tint = {.r = el.colorR, .g = el.colorG, .b = el.colorB}; + + // Triangle 0: top-left, top-right, bottom-left + { + auto& tri = balloc.allocateFragment(); + tri.primitive.pointA.x = x; tri.primitive.pointA.y = y; + tri.primitive.pointB.x = x + w; tri.primitive.pointB.y = y; + tri.primitive.pointC.x = x; tri.primitive.pointC.y = y + h; + tri.primitive.uvA.u = el.image.u0; tri.primitive.uvA.v = el.image.v0; + tri.primitive.uvB.u = el.image.u1; tri.primitive.uvB.v = el.image.v0; + tri.primitive.uvC.u = el.image.u0; tri.primitive.uvC.v = el.image.v1; + tri.primitive.tpage = tpage; + tri.primitive.clutIndex = clut; + tri.primitive.setColorA(tint); + tri.primitive.setColorB(tint); + tri.primitive.setColorC(tint); + tri.primitive.setOpaque(); + ot.insert(tri, 0); + } + // Triangle 1: top-right, bottom-right, bottom-left + { + auto& tri = balloc.allocateFragment(); + tri.primitive.pointA.x = x + w; tri.primitive.pointA.y = y; + tri.primitive.pointB.x = x + w; tri.primitive.pointB.y = y + h; + tri.primitive.pointC.x = x; tri.primitive.pointC.y = y + h; + tri.primitive.uvA.u = el.image.u1; tri.primitive.uvA.v = el.image.v0; + tri.primitive.uvB.u = el.image.u1; tri.primitive.uvB.v = el.image.v1; + tri.primitive.uvC.u = el.image.u0; tri.primitive.uvC.v = el.image.v1; + tri.primitive.tpage = tpage; + tri.primitive.clutIndex = clut; + tri.primitive.setColorA(tint); + tri.primitive.setColorB(tint); + tri.primitive.setColorC(tint); + tri.primitive.setOpaque(); + ot.insert(tri, 0); + } + break; + } + + case UIElementType::Text: { + // Queue text for phase 2 (after gpu.chain) + if (m_pendingTextCount < UI_MAX_ELEMENTS) { + uint8_t fi = (el.type == UIElementType::Text) ? el.textData.fontIndex : 0; + m_pendingTexts[m_pendingTextCount++] = { + x, y, + el.colorR, el.colorG, el.colorB, + fi, + el.textBuf + }; + } + break; + } + } +} + +// ============================================================================ +// Render phases +// ============================================================================ + +void UISystem::renderOT(psyqo::GPU& gpu, + psyqo::OrderingTable& ot, + psyqo::BumpAllocator& balloc) { + m_pendingTextCount = 0; + + // Canvases are pre-sorted by sortOrder (ascending = back first). + // Higher-sortOrder canvases insert at OT 0 later, appearing on top. + for (int i = 0; i < m_canvasCount; i++) { + UICanvas& cv = m_canvases[i]; + if (!cv.visible) continue; + for (int j = 0; j < cv.elementCount; j++) { + UIElement& el = cv.elements[j]; + if (!el.visible) continue; + renderElement(el, ot, balloc); + } + } +} + +void UISystem::renderText(psyqo::GPU& gpu) { + for (int i = 0; i < m_pendingTextCount; i++) { + auto& pt = m_pendingTexts[i]; + psyqo::FontBase* font = resolveFont(pt.fontIndex); + if (!font) continue; + font->chainprintf(gpu, + {{.x = pt.x, .y = pt.y}}, + {{.r = pt.r, .g = pt.g, .b = pt.b}}, + "%s", pt.text); + } +} + +// ============================================================================ +// Font support +// ============================================================================ + +psyqo::FontBase* UISystem::resolveFont(uint8_t fontIndex) { + if (fontIndex == 0 || fontIndex > m_fontCount) return m_systemFont; + return &m_customFonts[fontIndex - 1]; +} + +void UISystem::uploadFonts(psyqo::GPU& gpu) { + for (int i = 0; i < m_fontCount; i++) { + UIFontDesc& fd = m_fontDescs[i]; + if (!fd.pixelData || fd.pixelDataSize == 0) continue; + + // Upload 4bpp texture to VRAM + // 4bpp 256px wide = 64 VRAM hwords wide + Renderer::GetInstance().VramUpload( + reinterpret_cast(fd.pixelData), + (int16_t)fd.vramX, (int16_t)fd.vramY, + 64, (int16_t)fd.textureH); + + // Initialize the Font<2> instance for this custom font + m_customFonts[i].initialize(gpu, + {{.x = (int16_t)fd.vramX, .y = (int16_t)fd.vramY}}, + {{.x = (int16_t)fd.glyphW, .y = (int16_t)fd.glyphH}}); + } +} + +// ============================================================================ +// Canvas API +// ============================================================================ + +int UISystem::findCanvas(const char* name) const { + if (!name) return -1; + for (int i = 0; i < m_canvasCount; i++) { + if (m_canvases[i].name && ui_streq(m_canvases[i].name, name)) + return i; + } + return -1; +} + +void UISystem::setCanvasVisible(int idx, bool v) { + if (idx >= 0 && idx < m_canvasCount) + m_canvases[idx].visible = v; +} + +bool UISystem::isCanvasVisible(int idx) const { + if (idx >= 0 && idx < m_canvasCount) + return m_canvases[idx].visible; + return false; +} + +// ============================================================================ +// Element API +// ============================================================================ + +int UISystem::findElement(int canvasIdx, const char* name) const { + if (canvasIdx < 0 || canvasIdx >= m_canvasCount || !name) return -1; + const UICanvas& cv = m_canvases[canvasIdx]; + for (int i = 0; i < cv.elementCount; i++) { + if (cv.elements[i].name && ui_streq(cv.elements[i].name, name)) { + // Return flat handle: index into m_elements + int handle = (int)(cv.elements + i - m_elements); + return handle; + } + } + return -1; +} + +void UISystem::setElementVisible(int handle, bool v) { + if (handle >= 0 && handle < m_elementCount) + m_elements[handle].visible = v; +} + +bool UISystem::isElementVisible(int handle) const { + if (handle >= 0 && handle < m_elementCount) + return m_elements[handle].visible; + return false; +} + +void UISystem::setText(int handle, const char* text) { + if (handle < 0 || handle >= m_elementCount) return; + UIElement& el = m_elements[handle]; + if (el.type != UIElementType::Text) return; + if (!text) { el.textBuf[0] = '\0'; return; } + int i = 0; + while (i < UI_TEXT_BUF - 1 && text[i] != '\0') { + el.textBuf[i] = text[i]; + i++; + } + el.textBuf[i] = '\0'; +} + +const char* UISystem::getText(int handle) const { + if (handle < 0 || handle >= m_elementCount) return ""; + const UIElement& el = m_elements[handle]; + if (el.type != UIElementType::Text) return ""; + return el.textBuf; +} + +void UISystem::setProgress(int handle, uint8_t value) { + if (handle < 0 || handle >= m_elementCount) return; + UIElement& el = m_elements[handle]; + if (el.type != UIElementType::Progress) return; + if (value > 100) value = 100; + el.progress.value = value; +} + +void UISystem::setColor(int handle, uint8_t r, uint8_t g, uint8_t b) { + if (handle < 0 || handle >= m_elementCount) return; + m_elements[handle].colorR = r; + m_elements[handle].colorG = g; + m_elements[handle].colorB = b; +} + +void UISystem::getColor(int handle, uint8_t& r, uint8_t& g, uint8_t& b) const { + if (handle < 0 || handle >= m_elementCount) { r = g = b = 0; return; } + r = m_elements[handle].colorR; + g = m_elements[handle].colorG; + b = m_elements[handle].colorB; +} + +void UISystem::setPosition(int handle, int16_t x, int16_t y) { + if (handle < 0 || handle >= m_elementCount) return; + UIElement& el = m_elements[handle]; + el.x = x; + el.y = y; + // Zero out anchors to make position absolute + el.anchorMinX = 0; + el.anchorMinY = 0; + el.anchorMaxX = 0; + el.anchorMaxY = 0; +} + +void UISystem::getPosition(int handle, int16_t& x, int16_t& y) const { + if (handle < 0 || handle >= m_elementCount) { x = y = 0; return; } + // Resolve full layout to return actual screen position + int16_t rx, ry, rw, rh; + resolveLayout(m_elements[handle], rx, ry, rw, rh); + x = rx; + y = ry; +} + +void UISystem::setSize(int handle, int16_t w, int16_t h) { + if (handle < 0 || handle >= m_elementCount) return; + m_elements[handle].w = w; + m_elements[handle].h = h; + // Clear stretch anchors so size is explicit + m_elements[handle].anchorMaxX = m_elements[handle].anchorMinX; + m_elements[handle].anchorMaxY = m_elements[handle].anchorMinY; +} + +void UISystem::getSize(int handle, int16_t& w, int16_t& h) const { + if (handle < 0 || handle >= m_elementCount) { w = h = 0; return; } + int16_t rx, ry, rw, rh; + resolveLayout(m_elements[handle], rx, ry, rw, rh); + w = rw; + h = rh; +} + +void UISystem::setProgressColors(int handle, uint8_t bgR, uint8_t bgG, uint8_t bgB, + uint8_t fillR, uint8_t fillG, uint8_t fillB) { + if (handle < 0 || handle >= m_elementCount) return; + UIElement& el = m_elements[handle]; + if (el.type != UIElementType::Progress) return; + el.progress.bgR = bgR; + el.progress.bgG = bgG; + el.progress.bgB = bgB; + el.colorR = fillR; + el.colorG = fillG; + el.colorB = fillB; +} + +uint8_t UISystem::getProgress(int handle) const { + if (handle < 0 || handle >= m_elementCount) return 0; + const UIElement& el = m_elements[handle]; + if (el.type != UIElementType::Progress) return 0; + return el.progress.value; +} + +UIElementType UISystem::getElementType(int handle) const { + if (handle < 0 || handle >= m_elementCount) return UIElementType::Box; + return m_elements[handle].type; +} + +int UISystem::getCanvasElementCount(int canvasIdx) const { + if (canvasIdx < 0 || canvasIdx >= m_canvasCount) return 0; + return m_canvases[canvasIdx].elementCount; +} + +int UISystem::getCanvasElementHandle(int canvasIdx, int elementIndex) const { + if (canvasIdx < 0 || canvasIdx >= m_canvasCount) return -1; + const UICanvas& cv = m_canvases[canvasIdx]; + if (elementIndex < 0 || elementIndex >= cv.elementCount) return -1; + return (int)(cv.elements + elementIndex - m_elements); +} + +} // namespace psxsplash diff --git a/src/uisystem.hh b/src/uisystem.hh new file mode 100644 index 0000000..73c0bcb --- /dev/null +++ b/src/uisystem.hh @@ -0,0 +1,151 @@ +#pragma once + +#include +#include "vram_config.h" +#include "renderer.hh" +#include +#include +#include +#include +#include +#include + +namespace psxsplash { + +static constexpr int UI_MAX_CANVASES = 16; +static constexpr int UI_MAX_ELEMENTS = 128; +static constexpr int UI_TEXT_BUF = 64; +static constexpr int UI_MAX_FONTS = 4; // 0 = system font, 1-3 = custom + +enum class UIElementType : uint8_t { + Image = 0, + Box = 1, + Text = 2, + Progress = 3 +}; + +struct UIImageData { + uint8_t texpageX, texpageY; + uint16_t clutX, clutY; + uint8_t u0, v0, u1, v1; + uint8_t bitDepth; // 0=4bit, 1=8bit, 2=16bit +}; + +struct UIProgressData { + uint8_t bgR, bgG, bgB; + uint8_t value; // 0-100, mutable +}; + +struct UITextData { + uint8_t fontIndex; // 0 = system font, 1+ = custom font +}; + +struct UIElement { + UIElementType type; + bool visible; + const char* name; // points into splashpack data + int16_t x, y, w, h; // pixel rect / offset in PS1 screen space + uint8_t anchorMinX, anchorMinY, anchorMaxX, anchorMaxY; // 8.8 fixed + uint8_t colorR, colorG, colorB; + union { UIImageData image; UIProgressData progress; UITextData textData; }; + char textBuf[UI_TEXT_BUF]; // UIText: mutable, starts with default +}; + +struct UICanvas { + const char* name; + bool visible; + uint8_t sortOrder; + UIElement* elements; // slice into m_elements pool + uint8_t elementCount; +}; + +/// Font descriptor loaded from the splashpack (for VRAM upload). +struct UIFontDesc { + uint8_t glyphW, glyphH; + uint16_t vramX, vramY; + uint16_t textureH; + const uint8_t* pixelData; // raw 4bpp, points into splashpack + uint32_t pixelDataSize; +}; + +class UISystem { +public: + void init(psyqo::Font<>& systemFont); + + /// Called from SplashPackLoader after splashpack is loaded. + /// Parses font descriptors (before canvas data) and canvas/element tables. + void loadFromSplashpack(uint8_t* data, uint16_t canvasCount, + uint8_t fontCount, uint32_t tableOffset); + + /// Upload custom font textures to VRAM and initialize Font<> instances. + /// Must be called AFTER loadFromSplashpack and BEFORE first render. + void uploadFonts(psyqo::GPU& gpu); + + // Phase 1: Insert OT primitives for boxes, images, progress bars. + // Called BEFORE gpu.chain(ot) from inside the renderer. + void renderOT(psyqo::GPU& gpu, + psyqo::OrderingTable& ot, + psyqo::BumpAllocator& balloc); + + // Phase 2: Emit text via psyqo font chaining. + // Called AFTER gpu.chain(ot). + void renderText(psyqo::GPU& gpu); + + // Canvas API + int findCanvas(const char* name) const; + void setCanvasVisible(int idx, bool v); + bool isCanvasVisible(int idx) const; + + // Element API — returns flat handle into m_elements, or -1 + int findElement(int canvasIdx, const char* name) const; + void setElementVisible(int handle, bool v); + bool isElementVisible(int handle) const; + void setText(int handle, const char* text); + const char* getText(int handle) const; + void setProgress(int handle, uint8_t value); + void setColor(int handle, uint8_t r, uint8_t g, uint8_t b); + void getColor(int handle, uint8_t& r, uint8_t& g, uint8_t& b) const; + void setPosition(int handle, int16_t x, int16_t y); + void getPosition(int handle, int16_t& x, int16_t& y) const; + void setSize(int handle, int16_t w, int16_t h); + void getSize(int handle, int16_t& w, int16_t& h) const; + void setProgressColors(int handle, uint8_t bgR, uint8_t bgG, uint8_t bgB, + uint8_t fillR, uint8_t fillG, uint8_t fillB); + uint8_t getProgress(int handle) const; + UIElementType getElementType(int handle) const; + int getCanvasElementCount(int canvasIdx) const; + int getCanvasElementHandle(int canvasIdx, int elementIndex) const; + +private: + psyqo::Font<>* m_systemFont = nullptr; + + // Custom fonts: up to 3, each with 4 fragments for DMA chaining + psyqo::Font<4> m_customFonts[UI_MAX_FONTS - 1]; + UIFontDesc m_fontDescs[UI_MAX_FONTS - 1]; // descriptors from splashpack + int m_fontCount = 0; // number of custom fonts (0-3) + + UICanvas m_canvases[UI_MAX_CANVASES]; + UIElement m_elements[UI_MAX_ELEMENTS]; + int m_canvasCount = 0; + int m_elementCount = 0; + + // Per-frame pending text list (filled during renderOT, flushed in renderText) + struct PendingText { int16_t x, y; uint8_t r, g, b; uint8_t fontIndex; const char* text; }; + PendingText m_pendingTexts[UI_MAX_ELEMENTS]; + int m_pendingTextCount = 0; + + /// Resolve which Font to use for a given fontIndex. + psyqo::FontBase* resolveFont(uint8_t fontIndex); + + void resolveLayout(const UIElement& el, + int16_t& outX, int16_t& outY, + int16_t& outW, int16_t& outH) const; + + void renderElement(UIElement& el, + psyqo::OrderingTable& ot, + psyqo::BumpAllocator& balloc); + + static psyqo::PrimPieces::TPageAttr makeTPage(const UIImageData& img); +}; + +} // namespace psxsplash