broken ui system

This commit is contained in:
Jan Racek
2026-03-25 12:25:29 +01:00
parent 60a7063a17
commit f485ec36a8
14 changed files with 1309 additions and 18 deletions

View File

@@ -19,7 +19,8 @@ src/controls.cpp \
src/profiler.cpp \ src/profiler.cpp \
src/collision.cpp \ src/collision.cpp \
src/bvh.cpp \ src/bvh.cpp \
src/cutscene.cpp src/cutscene.cpp \
src/uisystem.cpp
CPPFLAGS += -DPCDRV_SUPPORT=1 CPPFLAGS += -DPCDRV_SUPPORT=1

View File

@@ -3,6 +3,7 @@
#include <psyqo/fixed-point.hh> #include <psyqo/fixed-point.hh>
#include <psyqo/soft-math.hh> #include <psyqo/soft-math.hh>
#include <psyqo/trigonometry.hh> #include <psyqo/trigonometry.hh>
#include "uisystem.hh"
namespace psxsplash { namespace psxsplash {
@@ -14,7 +15,8 @@ static bool cs_streq(const char* a, const char* b) {
return *a == *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_cutscenes = cutscenes;
m_count = count; m_count = count;
m_active = nullptr; m_active = nullptr;
@@ -22,6 +24,7 @@ void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioM
m_nextAudio = 0; m_nextAudio = 0;
m_camera = camera; m_camera = camera;
m_audio = audio; m_audio = audio;
m_uiSystem = uiSystem;
} }
bool CutscenePlayer::play(const char* name) { bool CutscenePlayer::play(const char* name) {
@@ -68,6 +71,38 @@ bool CutscenePlayer::play(const char* name) {
track.initialValues[0] = track.target->isActive() ? 1 : 0; track.initialValues[0] = track.target->isActive() ? 1 : 0;
} }
break; 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 // Advance frame
m_frame++; m_frame++;
if (m_frame >= m_active->totalFrames) { if (m_frame > m_active->totalFrames) {
m_active = nullptr; // Cutscene finished m_active = nullptr; // Cutscene finished
} }
} }
@@ -296,6 +331,63 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) {
track.target->setActive(activeVal != 0); track.target->setActive(activeVal != 0);
break; 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;
}
} }
} }

View File

@@ -11,6 +11,8 @@
namespace psxsplash { namespace psxsplash {
class UISystem; // Forward declaration
static constexpr int MAX_CUTSCENES = 16; static constexpr int MAX_CUTSCENES = 16;
static constexpr int MAX_TRACKS = 8; static constexpr int MAX_TRACKS = 8;
static constexpr int MAX_KEYFRAMES = 64; static constexpr int MAX_KEYFRAMES = 64;
@@ -22,6 +24,12 @@ enum class TrackType : uint8_t {
ObjectPosition = 2, ObjectPosition = 2,
ObjectRotationY = 3, ObjectRotationY = 3,
ObjectActive = 4, 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. /// Per-keyframe interpolation mode.
@@ -59,13 +67,16 @@ struct CutsceneTrack {
uint8_t keyframeCount; uint8_t keyframeCount;
uint8_t pad[2]; uint8_t pad[2];
CutsceneKeyframe* keyframes; // Points into splashpack data (resolved at load time) 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. /// 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 position tracks: fp12 x,y,z. For rotation tracks: raw angle values.
/// For ObjectActive: values[0] = 1 (active) or 0 (inactive). /// For ObjectActive: values[0] = 1 (active) or 0 (inactive).
int16_t initialValues[3]; int16_t initialValues[3];
int16_t _initPad;
}; };
struct Cutscene { struct Cutscene {
@@ -82,7 +93,8 @@ struct Cutscene {
class CutscenePlayer { class CutscenePlayer {
public: public:
/// Initialize with loaded cutscene data. Safe to pass nullptr/0 if no cutscenes. /// 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. /// Play cutscene by name. Returns false if not found.
bool play(const char* name); bool play(const char* name);
@@ -104,6 +116,7 @@ private:
uint8_t m_nextAudio = 0; uint8_t m_nextAudio = 0;
Camera* m_camera = nullptr; Camera* m_camera = nullptr;
AudioManager* m_audio = nullptr; AudioManager* m_audio = nullptr;
UISystem* m_uiSystem = nullptr;
psyqo::Trig<> m_trig; psyqo::Trig<> m_trig;

View File

@@ -4,6 +4,7 @@
#include "controls.hh" #include "controls.hh"
#include "camera.hh" #include "camera.hh"
#include "cutscene.hh" #include "cutscene.hh"
#include "uisystem.hh"
#include <psyqo/soft-math.hh> #include <psyqo/soft-math.hh>
#include <psyqo/trigonometry.hh> #include <psyqo/trigonometry.hh>
@@ -15,6 +16,7 @@ namespace psxsplash {
// Static member // Static member
SceneManager* LuaAPI::s_sceneManager = nullptr; SceneManager* LuaAPI::s_sceneManager = nullptr;
CutscenePlayer* LuaAPI::s_cutscenePlayer = nullptr; CutscenePlayer* LuaAPI::s_cutscenePlayer = nullptr;
UISystem* LuaAPI::s_uiSystem = nullptr;
// Scale factor: FixedPoint<12> stores 1.0 as raw 4096. // Scale factor: FixedPoint<12> stores 1.0 as raw 4096.
// Lua scripts work in world-space units (1 = one unit), so we convert. // Lua scripts work in world-space units (1 = one unit), so we convert.
@@ -36,9 +38,10 @@ static psyqo::Trig<> s_trig;
// REGISTRATION // 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_sceneManager = scene;
s_cutscenePlayer = cutscenePlayer; s_cutscenePlayer = cutscenePlayer;
s_uiSystem = uiSystem;
// ======================================================================== // ========================================================================
// ENTITY API // ENTITY API
@@ -281,6 +284,73 @@ void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cut
L.setField(-2, "IsPlaying"); L.setField(-2, "IsPlaying");
L.setGlobal("Cutscene"); 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; 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<lua_Number>(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<int>(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<int>(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<int>(lua.toNumber(1));
const char* name = lua.toString(2);
int handle = s_uiSystem->findElement(canvasIdx, name);
lua.pushNumber(static_cast<lua_Number>(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<int>(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<int>(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<int>(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<int>(lua.toNumber(1));
int value = static_cast<int>(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<int>(lua.toNumber(1));
lua.pushNumber(static_cast<lua_Number>(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<int>(lua.toNumber(1));
uint8_t r = static_cast<uint8_t>(lua.toNumber(2));
uint8_t g = static_cast<uint8_t>(lua.toNumber(3));
uint8_t b = static_cast<uint8_t>(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<int>(lua.toNumber(1));
int16_t x = static_cast<int16_t>(lua.toNumber(2));
int16_t y = static_cast<int16_t>(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<int>(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<int>(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<int>(lua.toNumber(1));
int16_t x, y;
s_uiSystem->getPosition(handle, x, y);
lua.pushNumber(static_cast<lua_Number>(x));
lua.pushNumber(static_cast<lua_Number>(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<int>(lua.toNumber(1));
int16_t w = static_cast<int16_t>(lua.toNumber(2));
int16_t h = static_cast<int16_t>(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<int>(lua.toNumber(1));
int16_t w, h;
s_uiSystem->getSize(handle, w, h);
lua.pushNumber(static_cast<lua_Number>(w));
lua.pushNumber(static_cast<lua_Number>(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<int>(lua.toNumber(1));
uint8_t bgR = static_cast<uint8_t>(lua.toNumber(2));
uint8_t bgG = static_cast<uint8_t>(lua.toNumber(3));
uint8_t bgB = static_cast<uint8_t>(lua.toNumber(4));
uint8_t fR = static_cast<uint8_t>(lua.toNumber(5));
uint8_t fG = static_cast<uint8_t>(lua.toNumber(6));
uint8_t fB = static_cast<uint8_t>(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<int>(lua.toNumber(1));
lua.pushNumber(static_cast<lua_Number>(static_cast<uint8_t>(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<int>(lua.toNumber(1));
lua.pushNumber(static_cast<lua_Number>(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<int>(lua.toNumber(1));
int elemIdx = static_cast<int>(lua.toNumber(2));
int handle = s_uiSystem->getCanvasElementHandle(canvasIdx, elemIdx);
lua.pushNumber(static_cast<lua_Number>(handle));
return 1;
}
} // namespace psxsplash } // namespace psxsplash

View File

@@ -8,6 +8,7 @@ namespace psxsplash {
class SceneManager; // Forward declaration class SceneManager; // Forward declaration
class CutscenePlayer; // Forward declaration class CutscenePlayer; // Forward declaration
class UISystem; // Forward declaration
/** /**
* Lua API - Provides game scripting functionality * Lua API - Provides game scripting functionality
@@ -24,7 +25,7 @@ class CutscenePlayer; // Forward declaration
class LuaAPI { class LuaAPI {
public: public:
// Initialize all API modules // 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 // Called once per frame to advance the Lua frame counter
static void IncrementFrameCount(); static void IncrementFrameCount();
@@ -39,6 +40,9 @@ private:
// Cutscene player pointer (set during RegisterAll) // Cutscene player pointer (set during RegisterAll)
static CutscenePlayer* s_cutscenePlayer; static CutscenePlayer* s_cutscenePlayer;
// UI system pointer (set during RegisterAll)
static UISystem* s_uiSystem;
// ======================================================================== // ========================================================================
// ENTITY API // ENTITY API
// ======================================================================== // ========================================================================
@@ -267,6 +271,31 @@ private:
// Cutscene.IsPlaying() -> boolean // Cutscene.IsPlaying() -> boolean
static int Cutscene_IsPlaying(lua_State* L); 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 // HELPERS
// ======================================================================== // ========================================================================

View File

@@ -52,6 +52,7 @@ void PSXSplash::prepare() {
void PSXSplash::createScene() { void PSXSplash::createScene() {
m_font.uploadSystemFont(gpu()); m_font.uploadSystemFont(gpu());
psxsplash::SceneManager::SetFont(&m_font);
pushScene(&mainScene); pushScene(&mainScene);
} }

View File

@@ -16,6 +16,7 @@
#include <psyqo/vector.hh> #include <psyqo/vector.hh>
#include "gtemath.hh" #include "gtemath.hh"
#include "uisystem.hh"
using namespace psyqo::fixed_point_literals; using namespace psyqo::fixed_point_literals;
using namespace psyqo::trig_literals; using namespace psyqo::trig_literals;
@@ -275,8 +276,10 @@ void psxsplash::Renderer::Render(eastl::vector<GameObject*>& objects) {
for (int i = 0; i < obj->polyCount; i++) for (int i = 0; i < obj->polyCount; i++)
processTriangle(obj->polygons[i], fogFarSZ, ot, balloc); 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.getNextClear(clear.primitive, m_clearcolor);
m_gpu.chain(clear); m_gpu.chain(ot); m_gpu.chain(clear); m_gpu.chain(ot);
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
m_frameCount++; m_frameCount++;
} }
@@ -307,8 +310,10 @@ void psxsplash::Renderer::RenderWithBVH(eastl::vector<GameObject*>& objects, con
} }
processTriangle(obj->polygons[ref.triangleIndex], fogFarSZ, ot, balloc); 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.getNextClear(clear.primitive, m_clearcolor);
m_gpu.chain(clear); m_gpu.chain(ot); m_gpu.chain(clear); m_gpu.chain(ot);
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
m_frameCount++; m_frameCount++;
} }
@@ -750,8 +755,10 @@ void psxsplash::Renderer::RenderWithRooms(eastl::vector<GameObject*>& objects,
} }
#endif #endif
if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc);
m_gpu.getNextClear(clear.primitive, m_clearcolor); m_gpu.getNextClear(clear.primitive, m_clearcolor);
m_gpu.chain(clear); m_gpu.chain(ot); m_gpu.chain(clear); m_gpu.chain(ot);
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
m_frameCount++; m_frameCount++;
} }

View File

@@ -20,6 +20,8 @@
namespace psxsplash { namespace psxsplash {
class UISystem; // Forward declaration
struct FogConfig { struct FogConfig {
bool enabled = false; bool enabled = false;
psyqo::Color color = {.r = 0, .g = 0, .b = 0}; 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, void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY,
int16_t width, int16_t height); int16_t width, int16_t height);
void SetUISystem(UISystem* ui) { m_uiSystem = ui; }
psyqo::GPU& getGPU() { return m_gpu; }
static Renderer& GetInstance() { static Renderer& GetInstance() {
psyqo::Kernel::assert(instance != nullptr, psyqo::Kernel::assert(instance != nullptr,
"Access to renderer was tried without prior initialization"); "Access to renderer was tried without prior initialization");
@@ -78,6 +83,8 @@ class Renderer final {
FogConfig m_fog; FogConfig m_fog;
psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0}; psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0};
UISystem* m_uiSystem = nullptr;
TriangleRef m_visibleRefs[MAX_VISIBLE_TRIANGLES]; TriangleRef m_visibleRefs[MAX_VISIBLE_TRIANGLES];
int m_frameCount = 0; int m_frameCount = 0;

View File

@@ -16,6 +16,9 @@ using namespace psyqo::fixed_point_literals;
using namespace psxsplash; using namespace psxsplash;
// Static member definition
psyqo::Font<>* psxsplash::SceneManager::s_font = nullptr;
void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData) { void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData) {
L.Reset(); L.Reset();
@@ -23,7 +26,7 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData) {
m_audio.init(); m_audio.init();
// Register the Lua API // Register the Lua API
LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer); LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer, &m_uiSystem);
#ifdef PSXSPLASH_PROFILER #ifdef PSXSPLASH_PROFILER
debug::Profiler::getInstance().initialize(); debug::Profiler::getInstance().initialize();
@@ -84,9 +87,56 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData) {
m_cutsceneCount > 0 ? m_cutscenes : nullptr, m_cutsceneCount > 0 ? m_cutscenes : nullptr,
m_cutsceneCount, m_cutsceneCount,
&m_currentCamera, &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<uint8_t>(track.trackType) >= 5;
if (!isUI || track.target == nullptr) continue;
const char* nameStr = reinterpret_cast<const char*>(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<int16_t>(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<char*>(sep);
*mutableSep = '\0';
int canvasIdx = m_uiSystem.findCanvas(nameStr);
*mutableSep = '/'; // Restore the separator
if (canvasIdx >= 0) {
track.uiHandle = static_cast<int16_t>(
m_uiSystem.findElement(canvasIdx, sep + 1));
}
}
}
}
}
} else {
Renderer::GetInstance().SetUISystem(nullptr);
}
m_playerPosition = sceneSetup.playerStartPosition; m_playerPosition = sceneSetup.playerStartPosition;
playerRotationX = 0.0_pi; playerRotationX = 0.0_pi;
@@ -607,6 +657,9 @@ void psxsplash::SceneManager::clearScene() {
m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player
// BVH, WorldCollision, and NavRegions will be overwritten by next load // 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) // Reset room/portal pointers (they point into splashpack data which is being freed)
m_rooms = nullptr; m_rooms = nullptr;
m_roomCount = 0; m_roomCount = 0;

View File

@@ -20,6 +20,7 @@
#include "luaapi.hh" #include "luaapi.hh"
#include "sceneloader.hh" #include "sceneloader.hh"
#include "cutscene.hh" #include "cutscene.hh"
#include "uisystem.hh"
namespace psxsplash { namespace psxsplash {
class SceneManager { class SceneManager {
@@ -27,6 +28,10 @@ class SceneManager {
void InitializeScene(uint8_t* splashpackData); void InitializeScene(uint8_t* splashpackData);
void GameTick(psyqo::GPU &gpu); 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) // Trigger event callbacks (called by CollisionSystem)
void fireTriggerEnter(uint16_t triggerObjIdx, uint16_t otherObjIdx); void fireTriggerEnter(uint16_t triggerObjIdx, uint16_t otherObjIdx);
void fireTriggerStay(uint16_t triggerObjIdx, uint16_t otherObjIdx); void fireTriggerStay(uint16_t triggerObjIdx, uint16_t otherObjIdx);
@@ -119,6 +124,9 @@ class SceneManager {
int m_cutsceneCount = 0; int m_cutsceneCount = 0;
CutscenePlayer m_cutscenePlayer; CutscenePlayer m_cutscenePlayer;
// UI system (v13+)
UISystem m_uiSystem;
psxsplash::Controls m_controls; psxsplash::Controls m_controls;
psxsplash::Camera m_currentCamera; psxsplash::Camera m_currentCamera;
@@ -141,6 +149,9 @@ class SceneManager {
bool freecam = false; bool freecam = false;
// Static font pointer (set from main.cpp)
static psyqo::Font<>* s_font;
// Scene transition state // Scene transition state
int m_currentSceneIndex = 0; int m_currentSceneIndex = 0;
int m_pendingSceneIndex = -1; // -1 = no pending load int m_pendingSceneIndex = -1; // -1 = no pending load

View File

@@ -83,8 +83,13 @@ struct SPLASHPACKFileHeader {
uint16_t cutsceneCount; uint16_t cutsceneCount;
uint16_t reserved_cs; uint16_t reserved_cs;
uint32_t cutsceneTableOffset; 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 { struct SPLASHPACKTextureAtlas {
uint32_t polygonsOffset; 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"); psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null");
psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast<psxsplash::SPLASHPACKFileHeader *>(data); psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast<psxsplash::SPLASHPACKFileHeader *>(data);
psyqo::Kernel::assert(__builtin_memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic"); 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.playerStartPosition = header->playerStartPos;
setup.playerStartRotation = header->playerStartRot; setup.playerStartRotation = header->playerStartRot;
@@ -354,10 +359,17 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
? reinterpret_cast<CutsceneKeyframe*>(data + kfOff) ? reinterpret_cast<CutsceneKeyframe*>(data + kfOff)
: nullptr; : nullptr;
// Resolve target object by name // Resolve target object by name (or store UI name for later resolution)
track.target = nullptr; track.target = nullptr;
track.uiHandle = -1;
if (objNameLen > 0 && objNameOff != 0) { if (objNameLen > 0 && objNameOff != 0) {
const char* objName = reinterpret_cast<const char*>(data + objNameOff); const char* objName = reinterpret_cast<const char*>(data + objNameOff);
bool isUITrack = static_cast<uint8_t>(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<GameObject*>(const_cast<char*>(objName));
} else {
for (size_t oi = 0; oi < setup.objectNames.size(); oi++) { for (size_t oi = 0; oi < setup.objectNames.size(); oi++) {
if (setup.objectNames[oi] && if (setup.objectNames[oi] &&
sp_streq(setup.objectNames[oi], objName)) { sp_streq(setup.objectNames[oi], objName)) {
@@ -365,6 +377,7 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
break; break;
} }
} }
}
// If not found, target stays nullptr — track will be skipped at runtime // 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].keyframeCount = 0;
cs.tracks[ti].keyframes = nullptr; cs.tracks[ti].keyframes = nullptr;
cs.tracks[ti].target = nullptr; cs.tracks[ti].target = nullptr;
cs.tracks[ti].uiHandle = -1;
cs.tracks[ti].initialValues[0] = 0; cs.tracks[ti].initialValues[0] = 0;
cs.tracks[ti].initialValues[1] = 0; cs.tracks[ti].initialValues[1] = 0;
cs.tracks[ti].initialValues[2] = 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 } // namespace psxsplash

View File

@@ -13,6 +13,7 @@
#include "audiomanager.hh" #include "audiomanager.hh"
#include "interactable.hh" #include "interactable.hh"
#include "cutscene.hh" #include "cutscene.hh"
#include "uisystem.hh"
namespace psxsplash { namespace psxsplash {
@@ -90,6 +91,11 @@ struct SplashpackSceneSetup {
// Cutscenes (version 12+) // Cutscenes (version 12+)
Cutscene loadedCutscenes[MAX_CUTSCENES]; Cutscene loadedCutscenes[MAX_CUTSCENES];
int cutsceneCount = 0; int cutsceneCount = 0;
// UI system (v13+)
uint16_t uiCanvasCount = 0;
uint8_t uiFontCount = 0;
uint32_t uiTableOffset = 0;
}; };
class SplashPackLoader { class SplashPackLoader {

581
src/uisystem.cpp Normal file
View File

@@ -0,0 +1,581 @@
#include "uisystem.hh"
#include <psyqo/kernel.hh>
#include <psyqo/primitives/common.hh>
#include <psyqo/primitives/misc.hh>
#include <psyqo/primitives/rectangles.hh>
#include <psyqo/primitives/triangles.hh>
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<uint16_t*>(ptr + 2);
fd.vramY = *reinterpret_cast<uint16_t*>(ptr + 4);
fd.textureH = *reinterpret_cast<uint16_t*>(ptr + 6);
uint32_t dataOff = *reinterpret_cast<uint32_t*>(ptr + 8);
fd.pixelDataSize = *reinterpret_cast<uint32_t*>(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<uint32_t*>(tablePtr); tablePtr += 4;
uint8_t nameLen = *tablePtr++;
uint8_t sortOrder = *tablePtr++;
uint8_t elementCount = *tablePtr++;
uint8_t flags = *tablePtr++;
uint32_t nameOffset = *reinterpret_cast<uint32_t*>(tablePtr); tablePtr += 4;
UICanvas& cv = m_canvases[ci];
cv.name = (nameLen > 0 && nameOffset != 0)
? reinterpret_cast<const char*>(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<UIElementType>(*elemPtr++);
uint8_t eFlags = *elemPtr++;
el.visible = (eFlags & 0x01) != 0;
uint8_t eNameLen = *elemPtr++;
elemPtr++; // pad0
uint32_t eNameOff = *reinterpret_cast<uint32_t*>(elemPtr); elemPtr += 4;
el.name = (eNameLen > 0 && eNameOff != 0)
? reinterpret_cast<const char*>(data + eNameOff)
: "";
// Layout (8 bytes)
el.x = *reinterpret_cast<int16_t*>(elemPtr); elemPtr += 2;
el.y = *reinterpret_cast<int16_t*>(elemPtr); elemPtr += 2;
el.w = *reinterpret_cast<int16_t*>(elemPtr); elemPtr += 2;
el.h = *reinterpret_cast<int16_t*>(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<uint8_t*>(&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<uint16_t*>(&typeData[2]);
el.image.clutY = *reinterpret_cast<uint16_t*>(&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<uint32_t*>(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<const char*>(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<Renderer::ORDERING_TABLE_SIZE>& ot,
psyqo::BumpAllocator<Renderer::BUMP_ALLOCATOR_SIZE>& balloc) {
int16_t x, y, w, h;
resolveLayout(el, x, y, w, h);
switch (el.type) {
case UIElementType::Box: {
auto& frag = balloc.allocateFragment<psyqo::Prim::Rectangle>();
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<psyqo::Prim::Rectangle>();
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<psyqo::Prim::Rectangle>();
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<psyqo::Prim::GouraudTexturedTriangle>();
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<psyqo::Prim::GouraudTexturedTriangle>();
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<Renderer::ORDERING_TABLE_SIZE>& ot,
psyqo::BumpAllocator<Renderer::BUMP_ALLOCATOR_SIZE>& 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<const uint16_t*>(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

151
src/uisystem.hh Normal file
View File

@@ -0,0 +1,151 @@
#pragma once
#include <stdint.h>
#include "vram_config.h"
#include "renderer.hh"
#include <psyqo/font.hh>
#include <psyqo/gpu.hh>
#include <psyqo/primitives/common.hh>
#include <psyqo/primitives/misc.hh>
#include <psyqo/primitives/rectangles.hh>
#include <psyqo/primitives/triangles.hh>
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<Renderer::ORDERING_TABLE_SIZE>& ot,
psyqo::BumpAllocator<Renderer::BUMP_ALLOCATOR_SIZE>& 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<Renderer::ORDERING_TABLE_SIZE>& ot,
psyqo::BumpAllocator<Renderer::BUMP_ALLOCATOR_SIZE>& balloc);
static psyqo::PrimPieces::TPageAttr makeTPage(const UIImageData& img);
};
} // namespace psxsplash