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

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