Compare commits
40 Commits
68084796df
...
lua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
428725d9ce | ||
|
|
9a82f9e3a1 | ||
| 7c344e2e37 | |||
| 7425d9a5b2 | |||
| 92d1a73248 | |||
|
|
9f3918c8c8 | ||
|
|
561ee9dd64 | ||
|
|
68cf8a7460 | ||
| 69aa4e079d | |||
| bb93ecdc5d | |||
|
|
b01b72751a | ||
|
|
bfab154547 | ||
|
|
85eb7e59d9 | ||
|
|
4ff7ecdd57 | ||
|
|
480323f5b9 | ||
|
|
090402f71a | ||
|
|
eacbf4de46 | ||
|
|
19bb2254f3 | ||
|
|
37ba4c85fe | ||
|
|
f485ec36a8 | ||
|
|
60a7063a17 | ||
|
|
e51c06b012 | ||
| 55c1d2c39b | |||
| 8151f3864c | |||
| 8bc15db7de | |||
| 5b761ab5bc | |||
| 09c5ad57b3 | |||
| 7e4532d846 | |||
| b5b0ae464c | |||
| f8ab161270 | |||
| 8699ea7845 | |||
| 441acbb6c9 | |||
| f79b69de0a | |||
| febb6f3f80 | |||
| bc0795ed29 | |||
| 651cbcdf55 | |||
| cc1b2c84ef | |||
| 9d1dd809b5 | |||
| 3c0699b081 | |||
| c960e28015 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -8,7 +8,13 @@ PSX.Dev-README.md
|
|||||||
*.a
|
*.a
|
||||||
|
|
||||||
.cache/
|
.cache/
|
||||||
.vscode/
|
.vscode/*
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/tasks.json
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.clang-format
|
.clang-format
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
LICENSEE.DAT
|
||||||
|
*.iso
|
||||||
|
system.cnf
|
||||||
|
iso.xml
|
||||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
|||||||
[submodule "third_party/nugget"]
|
[submodule "third_party/nugget"]
|
||||||
path = third_party/nugget
|
path = third_party/nugget
|
||||||
url = https://github.com/pcsx-redux/nugget.git
|
url = https://github.com/pcsx-redux/nugget
|
||||||
|
|||||||
29
.vscode/launch.json
vendored
Normal file
29
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug",
|
||||||
|
"type": "gdb",
|
||||||
|
"request": "attach",
|
||||||
|
"target": "localhost:3333",
|
||||||
|
"remote": true,
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"valuesFormatting": "parseText",
|
||||||
|
"stopAtConnect": true,
|
||||||
|
"gdbpath": "gdb-multiarch",
|
||||||
|
"windows": {
|
||||||
|
"gdbpath": "gdb-multiarch.exe"
|
||||||
|
},
|
||||||
|
"osx": {
|
||||||
|
"gdbpath": "gdb"
|
||||||
|
},
|
||||||
|
"executable": "${workspaceRoot}/${workspaceRootFolderName}.elf",
|
||||||
|
"autorun": [
|
||||||
|
"monitor reset shellhalt",
|
||||||
|
"load ${workspaceRootFolderName}.elf",
|
||||||
|
"tbreak main",
|
||||||
|
"continue"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
37
.vscode/tasks.json
vendored
Normal file
37
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Build Debug",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make -j12 BUILD=Debug",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": [
|
||||||
|
"$gcc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build Release",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make -j12",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": [
|
||||||
|
"$gcc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Clean",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "make clean",
|
||||||
|
"group": {
|
||||||
|
"kind": "build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
59
Makefile
59
Makefile
@@ -6,9 +6,62 @@ src/main.cpp \
|
|||||||
src/renderer.cpp \
|
src/renderer.cpp \
|
||||||
src/splashpack.cpp \
|
src/splashpack.cpp \
|
||||||
src/camera.cpp \
|
src/camera.cpp \
|
||||||
output.o
|
src/gtemath.cpp \
|
||||||
|
src/worldcollision.cpp \
|
||||||
|
src/navregion.cpp \
|
||||||
|
src/triclip.cpp \
|
||||||
|
src/lua.cpp \
|
||||||
|
src/luaapi.cpp \
|
||||||
|
src/scenemanager.cpp \
|
||||||
|
src/fileloader.cpp \
|
||||||
|
src/audiomanager.cpp \
|
||||||
|
src/controls.cpp \
|
||||||
|
src/profiler.cpp \
|
||||||
|
src/collision.cpp \
|
||||||
|
src/bvh.cpp \
|
||||||
|
src/cutscene.cpp \
|
||||||
|
src/interpolation.cpp \
|
||||||
|
src/animation.cpp \
|
||||||
|
src/uisystem.cpp \
|
||||||
|
src/loadingscreen.cpp \
|
||||||
|
src/memoverlay.cpp \
|
||||||
|
src/loadbuffer_patch.cpp
|
||||||
|
|
||||||
|
# LOADER=cdrom → CD-ROM backend (for ISO builds on real hardware)
|
||||||
|
# LOADER=pcdrv → PCdrv backend (default, emulator + SIO1)
|
||||||
|
ifeq ($(LOADER),cdrom)
|
||||||
|
CPPFLAGS += -DLOADER_CDROM
|
||||||
|
else
|
||||||
|
CPPFLAGS += -DPCDRV_SUPPORT=1
|
||||||
|
endif
|
||||||
|
|
||||||
|
# MEMOVERLAY=1 → Enable runtime heap/RAM usage overlay
|
||||||
|
ifeq ($(MEMOVERLAY),1)
|
||||||
|
CPPFLAGS += -DPSXSPLASH_MEMOVERLAY
|
||||||
|
endif
|
||||||
|
|
||||||
|
# FPSOVERLAY=1 → Enable runtime FPS overlay
|
||||||
|
ifeq ($(FPSOVERLAY), 1)
|
||||||
|
CPPFLAGS += -DPSXSPLASH_FPSOVERLAY
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifdef OT_SIZE
|
||||||
|
CPPFLAGS += -DOT_SIZE=$(OT_SIZE)
|
||||||
|
endif
|
||||||
|
ifdef BUMP_SIZE
|
||||||
|
CPPFLAGS += -DBUMP_SIZE=$(BUMP_SIZE)
|
||||||
|
endif
|
||||||
|
|
||||||
|
include third_party/nugget/psyqo-lua/psyqo-lua.mk
|
||||||
include third_party/nugget/psyqo/psyqo.mk
|
include third_party/nugget/psyqo/psyqo.mk
|
||||||
|
|
||||||
%.o: %.bin
|
# Redirect Lua's allocator through our OOM-guarded wrapper
|
||||||
$(PREFIX)-objcopy -I binary --set-section-alignment .data=4 --rename-section .data=.rodata,alloc,load,readonly,data,contents -O $(FORMAT) -B mips $< $@
|
LDFLAGS := $(subst psyqo_realloc,lua_oom_realloc,$(LDFLAGS))
|
||||||
|
|
||||||
|
# NOPARSER=1 → Use precompiled bytecode, strip Lua parser from runtime (~25KB savings)
|
||||||
|
ifeq ($(NOPARSER),1)
|
||||||
|
LIBRARIES := $(subst liblua.a,liblua-noparser.a,$(LIBRARIES))
|
||||||
|
# Wrap luaL_loadbufferx to intercept psyqo-lua's source-text FixedPoint
|
||||||
|
# metatable init and redirect it to pre-compiled bytecode.
|
||||||
|
LDFLAGS += -Wl,--wrap=luaL_loadbufferx
|
||||||
|
endif
|
||||||
|
|||||||
BIN
output.bin
BIN
output.bin
Binary file not shown.
297
src/animation.cpp
Normal file
297
src/animation.cpp
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
#include "animation.hh"
|
||||||
|
#include "interpolation.hh"
|
||||||
|
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/soft-math.hh>
|
||||||
|
#include "streq.hh"
|
||||||
|
#include "uisystem.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
void AnimationPlayer::init(Animation* animations, int count, UISystem* uiSystem) {
|
||||||
|
m_animations = animations;
|
||||||
|
m_animCount = count;
|
||||||
|
m_uiSystem = uiSystem;
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) {
|
||||||
|
m_slots[i].anim = nullptr;
|
||||||
|
m_slots[i].onCompleteRef = LUA_NOREF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation* AnimationPlayer::findByName(const char* name) const {
|
||||||
|
if (!name || !m_animations) return nullptr;
|
||||||
|
for (int i = 0; i < m_animCount; i++) {
|
||||||
|
if (m_animations[i].name && streq(m_animations[i].name, name))
|
||||||
|
return &m_animations[i];
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AnimationPlayer::play(const char* name, bool loop) {
|
||||||
|
Animation* anim = findByName(name);
|
||||||
|
if (!anim) return false;
|
||||||
|
|
||||||
|
// Find a free slot
|
||||||
|
int freeSlot = -1;
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) {
|
||||||
|
if (!m_slots[i].anim) {
|
||||||
|
freeSlot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (freeSlot < 0) return false;
|
||||||
|
|
||||||
|
ActiveSlot& slot = m_slots[freeSlot];
|
||||||
|
slot.anim = anim;
|
||||||
|
slot.frame = 0;
|
||||||
|
slot.loop = loop;
|
||||||
|
// onCompleteRef is set separately via setOnCompleteRef before play()
|
||||||
|
|
||||||
|
captureInitialValues(anim);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationPlayer::stop(const char* name) {
|
||||||
|
if (!name) return;
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) {
|
||||||
|
if (m_slots[i].anim && m_slots[i].anim->name && streq(m_slots[i].anim->name, name)) {
|
||||||
|
fireSlotComplete(m_slots[i]);
|
||||||
|
m_slots[i].anim = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationPlayer::stopAll() {
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) {
|
||||||
|
if (m_slots[i].anim) {
|
||||||
|
fireSlotComplete(m_slots[i]);
|
||||||
|
m_slots[i].anim = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AnimationPlayer::isPlaying(const char* name) const {
|
||||||
|
if (!name) return false;
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) {
|
||||||
|
if (m_slots[i].anim && m_slots[i].anim->name && streq(m_slots[i].anim->name, name))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationPlayer::setOnCompleteRef(const char* name, int ref) {
|
||||||
|
// Find the most recently started slot for this animation (highest index with frame 0)
|
||||||
|
// Fallback: find first slot with this name
|
||||||
|
for (int i = MAX_SIMULTANEOUS_ANIMS - 1; i >= 0; i--) {
|
||||||
|
if (m_slots[i].anim && m_slots[i].anim->name && streq(m_slots[i].anim->name, name)) {
|
||||||
|
m_slots[i].onCompleteRef = ref;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationPlayer::tick() {
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_ANIMS; i++) {
|
||||||
|
ActiveSlot& slot = m_slots[i];
|
||||||
|
if (!slot.anim) continue;
|
||||||
|
|
||||||
|
// Apply all tracks
|
||||||
|
for (uint8_t ti = 0; ti < slot.anim->trackCount; ti++) {
|
||||||
|
applyTrack(slot.anim->tracks[ti], slot.frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.frame++;
|
||||||
|
if (slot.frame > slot.anim->totalFrames) {
|
||||||
|
if (slot.loop) {
|
||||||
|
slot.frame = 0;
|
||||||
|
} else {
|
||||||
|
Animation* finished = slot.anim;
|
||||||
|
slot.anim = nullptr;
|
||||||
|
fireSlotComplete(slot);
|
||||||
|
(void)finished;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationPlayer::captureInitialValues(Animation* anim) {
|
||||||
|
for (uint8_t ti = 0; ti < anim->trackCount; ti++) {
|
||||||
|
CutsceneTrack& track = anim->tracks[ti];
|
||||||
|
track.initialValues[0] = track.initialValues[1] = track.initialValues[2] = 0;
|
||||||
|
switch (track.trackType) {
|
||||||
|
case TrackType::ObjectPosition:
|
||||||
|
if (track.target) {
|
||||||
|
track.initialValues[0] = (int16_t)track.target->position.x.value;
|
||||||
|
track.initialValues[1] = (int16_t)track.target->position.y.value;
|
||||||
|
track.initialValues[2] = (int16_t)track.target->position.z.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TrackType::ObjectRotation:
|
||||||
|
break;
|
||||||
|
case TrackType::ObjectActive:
|
||||||
|
if (track.target) {
|
||||||
|
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;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationPlayer::applyTrack(CutsceneTrack& track, uint16_t frame) {
|
||||||
|
if (track.keyframeCount == 0 || !track.keyframes) return;
|
||||||
|
|
||||||
|
int16_t out[3];
|
||||||
|
|
||||||
|
switch (track.trackType) {
|
||||||
|
case TrackType::ObjectPosition: {
|
||||||
|
if (!track.target) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, track.initialValues, out);
|
||||||
|
track.target->position.x.value = (int32_t)out[0];
|
||||||
|
track.target->position.y.value = (int32_t)out[1];
|
||||||
|
track.target->position.z.value = (int32_t)out[2];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::ObjectRotation: {
|
||||||
|
if (!track.target) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, track.initialValues, out);
|
||||||
|
psyqo::Angle rx, ry, rz;
|
||||||
|
rx.value = (int32_t)out[0];
|
||||||
|
ry.value = (int32_t)out[1];
|
||||||
|
rz.value = (int32_t)out[2];
|
||||||
|
auto matY = psyqo::SoftMath::generateRotationMatrix33(ry, psyqo::SoftMath::Axis::Y, m_trig);
|
||||||
|
auto matX = psyqo::SoftMath::generateRotationMatrix33(rx, psyqo::SoftMath::Axis::X, m_trig);
|
||||||
|
auto matZ = psyqo::SoftMath::generateRotationMatrix33(rz, psyqo::SoftMath::Axis::Z, m_trig);
|
||||||
|
auto temp = psyqo::SoftMath::multiplyMatrix33(matY, matX);
|
||||||
|
track.target->rotation = psyqo::SoftMath::multiplyMatrix33(temp, matZ);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::ObjectActive: {
|
||||||
|
if (!track.target) return;
|
||||||
|
CutsceneKeyframe* kf = track.keyframes;
|
||||||
|
uint8_t count = track.keyframeCount;
|
||||||
|
int16_t activeVal = (count > 0 && frame < kf[0].getFrame())
|
||||||
|
? track.initialValues[0]
|
||||||
|
: kf[0].values[0];
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
if (kf[i].getFrame() <= frame) {
|
||||||
|
activeVal = kf[i].values[0];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
track.target->setActive(activeVal != 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::UICanvasVisible: {
|
||||||
|
if (!m_uiSystem) return;
|
||||||
|
CutsceneKeyframe* kf = track.keyframes;
|
||||||
|
uint8_t count = track.keyframeCount;
|
||||||
|
int16_t val = (count > 0 && frame < kf[0].getFrame())
|
||||||
|
? track.initialValues[0] : kf[0].values[0];
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
if (kf[i].getFrame() <= 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 && frame < kf[0].getFrame())
|
||||||
|
? track.initialValues[0] : kf[0].values[0];
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
if (kf[i].getFrame() <= frame) val = kf[i].values[0];
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
m_uiSystem->setElementVisible(track.uiHandle, val != 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::UIProgress: {
|
||||||
|
if (!m_uiSystem) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, 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;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, track.initialValues, out);
|
||||||
|
m_uiSystem->setPosition(track.uiHandle, out[0], out[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::UIColor: {
|
||||||
|
if (!m_uiSystem) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, frame, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationPlayer::fireSlotComplete(ActiveSlot& slot) {
|
||||||
|
if (slot.onCompleteRef == LUA_NOREF || !m_luaState) return;
|
||||||
|
psyqo::Lua L(m_luaState);
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, slot.onCompleteRef);
|
||||||
|
if (L.isFunction(-1)) {
|
||||||
|
if (L.pcall(0, 0) != LUA_OK) {
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
luaL_unref(m_luaState, LUA_REGISTRYINDEX, slot.onCompleteRef);
|
||||||
|
slot.onCompleteRef = LUA_NOREF;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
71
src/animation.hh
Normal file
71
src/animation.hh
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/trigonometry.hh>
|
||||||
|
|
||||||
|
#include "cutscene.hh"
|
||||||
|
#include <psyqo-lua/lua.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
class UISystem;
|
||||||
|
|
||||||
|
static constexpr int MAX_ANIMATIONS = 16;
|
||||||
|
static constexpr int MAX_ANIM_TRACKS = 8;
|
||||||
|
static constexpr int MAX_SIMULTANEOUS_ANIMS = 8;
|
||||||
|
|
||||||
|
struct Animation {
|
||||||
|
const char* name;
|
||||||
|
uint16_t totalFrames;
|
||||||
|
uint8_t trackCount;
|
||||||
|
uint8_t pad;
|
||||||
|
CutsceneTrack tracks[MAX_ANIM_TRACKS];
|
||||||
|
};
|
||||||
|
|
||||||
|
class AnimationPlayer {
|
||||||
|
public:
|
||||||
|
void init(Animation* animations, int count, UISystem* uiSystem = nullptr);
|
||||||
|
|
||||||
|
/// Play animation by name. Returns false if not found or no free slots.
|
||||||
|
bool play(const char* name, bool loop = false);
|
||||||
|
|
||||||
|
/// Stop all instances of animation by name.
|
||||||
|
void stop(const char* name);
|
||||||
|
|
||||||
|
/// Stop all running animations.
|
||||||
|
void stopAll();
|
||||||
|
|
||||||
|
/// True if any instance of the named animation is playing.
|
||||||
|
bool isPlaying(const char* name) const;
|
||||||
|
|
||||||
|
/// Set a Lua callback for the next play() call.
|
||||||
|
void setOnCompleteRef(const char* name, int ref);
|
||||||
|
|
||||||
|
/// Set the lua_State for callbacks.
|
||||||
|
void setLuaState(lua_State* L) { m_luaState = L; }
|
||||||
|
|
||||||
|
/// Advance all active animations one frame.
|
||||||
|
void tick();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ActiveSlot {
|
||||||
|
Animation* anim = nullptr;
|
||||||
|
uint16_t frame = 0;
|
||||||
|
bool loop = false;
|
||||||
|
int onCompleteRef = LUA_NOREF;
|
||||||
|
};
|
||||||
|
|
||||||
|
Animation* m_animations = nullptr;
|
||||||
|
int m_animCount = 0;
|
||||||
|
ActiveSlot m_slots[MAX_SIMULTANEOUS_ANIMS];
|
||||||
|
UISystem* m_uiSystem = nullptr;
|
||||||
|
lua_State* m_luaState = nullptr;
|
||||||
|
psyqo::Trig<> m_trig;
|
||||||
|
|
||||||
|
Animation* findByName(const char* name) const;
|
||||||
|
void applyTrack(CutsceneTrack& track, uint16_t frame);
|
||||||
|
void captureInitialValues(Animation* anim);
|
||||||
|
void fireSlotComplete(ActiveSlot& slot);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
183
src/audiomanager.cpp
Normal file
183
src/audiomanager.cpp
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#include "audiomanager.hh"
|
||||||
|
|
||||||
|
#include "common/hardware/dma.h"
|
||||||
|
#include "common/hardware/spu.h"
|
||||||
|
#include <psyqo/kernel.hh>
|
||||||
|
#include <psyqo/spu.hh>
|
||||||
|
#include <psyqo/xprintf.h>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
uint16_t AudioManager::volToHw(int v) {
|
||||||
|
if (v <= 0)
|
||||||
|
return 0;
|
||||||
|
if (v >= 128)
|
||||||
|
return 0x3fff;
|
||||||
|
return static_cast<uint16_t>((v * 0x3fff) / 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioManager::init() {
|
||||||
|
psyqo::SPU::initialize();
|
||||||
|
|
||||||
|
m_nextAddr = SPU_RAM_START;
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_AUDIO_CLIPS; i++) {
|
||||||
|
m_clips[i].loaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioManager::reset() {
|
||||||
|
stopAll();
|
||||||
|
for (int i = 0; i < MAX_AUDIO_CLIPS; i++) {
|
||||||
|
m_clips[i].loaded = false;
|
||||||
|
}
|
||||||
|
m_nextAddr = SPU_RAM_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AudioManager::loadClip(int clipIndex, const uint8_t *adpcmData,
|
||||||
|
uint32_t sizeBytes, uint16_t sampleRate,
|
||||||
|
bool loop) {
|
||||||
|
if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS)
|
||||||
|
return false;
|
||||||
|
if (!adpcmData || sizeBytes == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// check for and skip VAG header if present
|
||||||
|
if (sizeBytes >= 48) {
|
||||||
|
const char *magic = reinterpret_cast<const char *>(adpcmData);
|
||||||
|
if (magic[0] == 'V' && magic[1] == 'A' && magic[2] == 'G' &&
|
||||||
|
magic[3] == 'p') {
|
||||||
|
adpcmData += 48;
|
||||||
|
sizeBytes -= 48;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t addr = (m_nextAddr + 15) & ~15u;
|
||||||
|
uint32_t alignedSize = (sizeBytes + 15) & ~15u;
|
||||||
|
|
||||||
|
if (addr + alignedSize > SPU_RAM_END) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *src = adpcmData;
|
||||||
|
uint32_t remaining = alignedSize;
|
||||||
|
uint32_t dstAddr = addr;
|
||||||
|
while (remaining > 0) {
|
||||||
|
uint32_t bytesThisRound = (remaining > 65520u) ? 65520u : remaining;
|
||||||
|
bytesThisRound &= ~15u; // 16-byte block alignment
|
||||||
|
if (bytesThisRound == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
uint16_t dmaSizeParam = (uint16_t)(bytesThisRound / 4);
|
||||||
|
psyqo::SPU::dmaWrite(dstAddr, src, dmaSizeParam, 4);
|
||||||
|
|
||||||
|
while (DMA_CTRL[DMA_SPU].CHCR & (1 << 24)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
src += bytesThisRound;
|
||||||
|
dstAddr += bytesThisRound;
|
||||||
|
remaining -= bytesThisRound;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPU_CTRL &= ~(0b11 << 4);
|
||||||
|
|
||||||
|
m_clips[clipIndex].spuAddr = addr;
|
||||||
|
m_clips[clipIndex].size = sizeBytes;
|
||||||
|
m_clips[clipIndex].sampleRate = sampleRate;
|
||||||
|
m_clips[clipIndex].loop = loop;
|
||||||
|
m_clips[clipIndex].loaded = true;
|
||||||
|
|
||||||
|
m_nextAddr = addr + alignedSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioManager::play(int clipIndex, int volume, int pan) {
|
||||||
|
if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS ||
|
||||||
|
!m_clips[clipIndex].loaded) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ch = psyqo::SPU::getNextFreeChannel();
|
||||||
|
if (ch == psyqo::SPU::NO_FREE_CHANNEL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
const AudioClip &clip = m_clips[clipIndex];
|
||||||
|
|
||||||
|
uint16_t vol = volToHw(volume);
|
||||||
|
uint16_t leftVol = vol;
|
||||||
|
uint16_t rightVol = vol;
|
||||||
|
if (pan != 64) {
|
||||||
|
int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan);
|
||||||
|
leftVol = (uint16_t)((uint32_t)vol * (127 - p) / 127);
|
||||||
|
rightVol = (uint16_t)((uint32_t)vol * p / 127);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr uint16_t DUMMY_SPU_ADDR = 0x1000;
|
||||||
|
if (clip.loop) {
|
||||||
|
SPU_VOICES[ch].sampleRepeatAddr = static_cast<uint16_t>(clip.spuAddr / 8);
|
||||||
|
} else {
|
||||||
|
SPU_VOICES[ch].sampleRepeatAddr = DUMMY_SPU_ADDR / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
psyqo::SPU::ChannelPlaybackConfig config;
|
||||||
|
config.sampleRate.value =
|
||||||
|
static_cast<uint16_t>(((uint32_t)clip.sampleRate << 12) / 44100);
|
||||||
|
config.volumeLeft = leftVol;
|
||||||
|
config.volumeRight = rightVol;
|
||||||
|
config.adsr = DEFAULT_ADSR;
|
||||||
|
|
||||||
|
if (ch > 15) {
|
||||||
|
SPU_KEY_OFF_HIGH = 1 << (ch - 16);
|
||||||
|
} else {
|
||||||
|
SPU_KEY_OFF_LOW = 1 << ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPU_VOICES[ch].volumeLeft = config.volumeLeft;
|
||||||
|
SPU_VOICES[ch].volumeRight = config.volumeRight;
|
||||||
|
SPU_VOICES[ch].sampleRate = config.sampleRate.value;
|
||||||
|
SPU_VOICES[ch].sampleStartAddr = static_cast<uint16_t>(clip.spuAddr / 8);
|
||||||
|
SPU_VOICES[ch].ad = config.adsr & 0xFFFF;
|
||||||
|
SPU_VOICES[ch].sr = (config.adsr >> 16) & 0xFFFF;
|
||||||
|
|
||||||
|
if (ch > 15) {
|
||||||
|
SPU_KEY_ON_HIGH = 1 << (ch - 16);
|
||||||
|
} else {
|
||||||
|
SPU_KEY_ON_LOW = 1 << ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<int>(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioManager::stopVoice(int channel) {
|
||||||
|
if (channel < 0 || channel >= MAX_VOICES)
|
||||||
|
return;
|
||||||
|
psyqo::SPU::silenceChannels(1u << channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioManager::stopAll() { psyqo::SPU::silenceChannels(0x00FFFFFFu); }
|
||||||
|
|
||||||
|
void AudioManager::setVoiceVolume(int channel, int volume, int pan) {
|
||||||
|
if (channel < 0 || channel >= MAX_VOICES)
|
||||||
|
return;
|
||||||
|
uint16_t vol = volToHw(volume);
|
||||||
|
if (pan == 64) {
|
||||||
|
SPU_VOICES[channel].volumeLeft = vol;
|
||||||
|
SPU_VOICES[channel].volumeRight = vol;
|
||||||
|
} else {
|
||||||
|
int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan);
|
||||||
|
SPU_VOICES[channel].volumeLeft =
|
||||||
|
(uint16_t)((uint32_t)vol * (127 - p) / 127);
|
||||||
|
SPU_VOICES[channel].volumeRight = (uint16_t)((uint32_t)vol * p / 127);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int AudioManager::getLoadedClipCount() const {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < MAX_AUDIO_CLIPS; i++) {
|
||||||
|
if (m_clips[i].loaded)
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
78
src/audiomanager.hh
Normal file
78
src/audiomanager.hh
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
static constexpr int MAX_AUDIO_CLIPS = 32;
|
||||||
|
|
||||||
|
static constexpr int MAX_VOICES = 24;
|
||||||
|
|
||||||
|
static constexpr uint32_t SPU_RAM_START = 0x1010;
|
||||||
|
static constexpr uint32_t SPU_RAM_END = 0x80000;
|
||||||
|
|
||||||
|
static constexpr uint32_t DEFAULT_ADSR = 0x000A000F;
|
||||||
|
|
||||||
|
struct AudioClip {
|
||||||
|
uint32_t spuAddr;
|
||||||
|
uint32_t size;
|
||||||
|
uint16_t sampleRate;
|
||||||
|
bool loop;
|
||||||
|
bool loaded;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Manages SPU voices and audio clip playback.
|
||||||
|
///
|
||||||
|
/// init()
|
||||||
|
/// loadClip(index, data, size, rate, loop) -> bool
|
||||||
|
/// play(clipIndex) -> channel
|
||||||
|
/// play(clipIndex, volume, pan) -> channel
|
||||||
|
/// stopVoice(channel)
|
||||||
|
/// stopAll()
|
||||||
|
/// setVoiceVolume(channel, vol, pan)
|
||||||
|
///
|
||||||
|
/// Volume is 0-128 (0=silent, 128=max). Pan is 0-127 (64=center).
|
||||||
|
class AudioManager {
|
||||||
|
public:
|
||||||
|
/// Initialize SPU hardware and reset state
|
||||||
|
void init();
|
||||||
|
|
||||||
|
/// Upload ADPCM data to SPU RAM and register as clip index.
|
||||||
|
/// Data must be 16-byte aligned. Returns true on success.
|
||||||
|
bool loadClip(int clipIndex, const uint8_t *adpcmData, uint32_t sizeBytes,
|
||||||
|
uint16_t sampleRate, bool loop);
|
||||||
|
|
||||||
|
/// Play a clip by index. Returns channel (0-23), or -1 if full.
|
||||||
|
/// Volume: 0-128 (128=max). Pan: 0 (left) to 127 (right), 64 = center.
|
||||||
|
int play(int clipIndex, int volume = 128, int pan = 64);
|
||||||
|
|
||||||
|
/// Stop a specific channel
|
||||||
|
void stopVoice(int channel);
|
||||||
|
|
||||||
|
/// Stop all playing channels
|
||||||
|
void stopAll();
|
||||||
|
|
||||||
|
/// Set volume/pan on a playing channel
|
||||||
|
void setVoiceVolume(int channel, int volume, int pan = 64);
|
||||||
|
|
||||||
|
/// Get total SPU RAM used by loaded clips
|
||||||
|
uint32_t getUsedSPURam() const { return m_nextAddr - SPU_RAM_START; }
|
||||||
|
|
||||||
|
/// Get total SPU RAM available
|
||||||
|
uint32_t getTotalSPURam() const { return SPU_RAM_END - SPU_RAM_START; }
|
||||||
|
|
||||||
|
/// Get number of loaded clips
|
||||||
|
int getLoadedClipCount() const;
|
||||||
|
|
||||||
|
/// Reset all clips and free SPU RAM
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Convert 0-128 volume to hardware 0-0x3FFF (fixed-volume mode)
|
||||||
|
static uint16_t volToHw(int v);
|
||||||
|
|
||||||
|
AudioClip m_clips[MAX_AUDIO_CLIPS];
|
||||||
|
uint32_t m_nextAddr = SPU_RAM_START;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
124
src/bvh.cpp
Normal file
124
src/bvh.cpp
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#include "bvh.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
void BVHManager::initialize(const BVHNode *nodes, uint16_t nodeCount,
|
||||||
|
const TriangleRef *triangleRefs,
|
||||||
|
uint16_t triangleRefCount) {
|
||||||
|
m_nodes = nodes;
|
||||||
|
m_nodeCount = nodeCount;
|
||||||
|
m_triangleRefs = triangleRefs;
|
||||||
|
m_triangleRefCount = triangleRefCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int BVHManager::cullFrustum(const Frustum &frustum, TriangleRef *outRefs,
|
||||||
|
int maxRefs) const {
|
||||||
|
if (!isLoaded() || m_nodeCount == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return traverseFrustum(0, frustum, outRefs, 0, maxRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
int BVHManager::traverseFrustum(int nodeIndex, const Frustum &frustum,
|
||||||
|
TriangleRef *outRefs, int currentCount,
|
||||||
|
int maxRefs) const {
|
||||||
|
if (nodeIndex < 0 || nodeIndex >= m_nodeCount)
|
||||||
|
return currentCount;
|
||||||
|
if (currentCount >= maxRefs)
|
||||||
|
return currentCount;
|
||||||
|
|
||||||
|
const BVHNode &node = m_nodes[nodeIndex];
|
||||||
|
|
||||||
|
if (!frustum.testAABB(node)) {
|
||||||
|
return currentCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.isLeaf()) {
|
||||||
|
int count = node.triangleCount;
|
||||||
|
int available = maxRefs - currentCount;
|
||||||
|
if (count > available)
|
||||||
|
count = available;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i];
|
||||||
|
}
|
||||||
|
return currentCount + count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.leftChild != 0xFFFF) {
|
||||||
|
currentCount = traverseFrustum(node.leftChild, frustum, outRefs,
|
||||||
|
currentCount, maxRefs);
|
||||||
|
}
|
||||||
|
if (node.rightChild != 0xFFFF) {
|
||||||
|
currentCount = traverseFrustum(node.rightChild, frustum, outRefs,
|
||||||
|
currentCount, maxRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BVHManager::queryRegion(int32_t minX, int32_t minY, int32_t minZ,
|
||||||
|
int32_t maxX, int32_t maxY, int32_t maxZ,
|
||||||
|
TriangleRef *outRefs, int maxRefs) const {
|
||||||
|
if (!isLoaded() || m_nodeCount == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return traverseRegion(0, minX, minY, minZ, maxX, maxY, maxZ, outRefs, 0,
|
||||||
|
maxRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
int BVHManager::traverseRegion(int nodeIndex, int32_t qMinX, int32_t qMinY,
|
||||||
|
int32_t qMinZ, int32_t qMaxX, int32_t qMaxY,
|
||||||
|
int32_t qMaxZ, TriangleRef *outRefs,
|
||||||
|
int currentCount, int maxRefs) const {
|
||||||
|
if (nodeIndex < 0 || nodeIndex >= m_nodeCount)
|
||||||
|
return currentCount;
|
||||||
|
if (currentCount >= maxRefs)
|
||||||
|
return currentCount;
|
||||||
|
|
||||||
|
const BVHNode &node = m_nodes[nodeIndex];
|
||||||
|
|
||||||
|
if (!aabbOverlap(node, qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ)) {
|
||||||
|
return currentCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.isLeaf()) {
|
||||||
|
int count = node.triangleCount;
|
||||||
|
int available = maxRefs - currentCount;
|
||||||
|
if (count > available)
|
||||||
|
count = available;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i];
|
||||||
|
}
|
||||||
|
return currentCount + count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.leftChild != 0xFFFF) {
|
||||||
|
currentCount =
|
||||||
|
traverseRegion(node.leftChild, qMinX, qMinY, qMinZ, qMaxX, qMaxY,
|
||||||
|
qMaxZ, outRefs, currentCount, maxRefs);
|
||||||
|
}
|
||||||
|
if (node.rightChild != 0xFFFF) {
|
||||||
|
currentCount =
|
||||||
|
traverseRegion(node.rightChild, qMinX, qMinY, qMinZ, qMaxX, qMaxY,
|
||||||
|
qMaxZ, outRefs, currentCount, maxRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BVHManager::aabbOverlap(const BVHNode &node, int32_t qMinX, int32_t qMinY,
|
||||||
|
int32_t qMinZ, int32_t qMaxX, int32_t qMaxY,
|
||||||
|
int32_t qMaxZ) {
|
||||||
|
if (node.maxX < qMinX || node.minX > qMaxX)
|
||||||
|
return false;
|
||||||
|
if (node.maxY < qMinY || node.minY > qMaxY)
|
||||||
|
return false;
|
||||||
|
if (node.maxZ < qMinZ || node.minZ > qMaxZ)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
159
src/bvh.hh
Normal file
159
src/bvh.hh
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
/// Triangle reference - points to a specific triangle in a specific object
|
||||||
|
struct TriangleRef {
|
||||||
|
uint16_t objectIndex;
|
||||||
|
uint16_t triangleIndex;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(TriangleRef) == 4, "TriangleRef must be 4 bytes");
|
||||||
|
|
||||||
|
/// BVH Node - stored in binary file
|
||||||
|
struct BVHNode {
|
||||||
|
int32_t minX, minY, minZ;
|
||||||
|
int32_t maxX, maxY, maxZ;
|
||||||
|
|
||||||
|
uint16_t leftChild;
|
||||||
|
uint16_t rightChild;
|
||||||
|
|
||||||
|
uint16_t firstTriangle;
|
||||||
|
uint16_t triangleCount;
|
||||||
|
|
||||||
|
bool isLeaf() const {
|
||||||
|
return leftChild == 0xFFFF && rightChild == 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool testPlane(int32_t nx, int32_t ny, int32_t nz, int32_t d) const {
|
||||||
|
int32_t px = (nx >= 0) ? maxX : minX;
|
||||||
|
int32_t py = (ny >= 0) ? maxY : minY;
|
||||||
|
int32_t pz = (nz >= 0) ? maxZ : minZ;
|
||||||
|
|
||||||
|
int64_t dot = ((int64_t)px * nx + (int64_t)py * ny + (int64_t)pz * nz) >> 12;
|
||||||
|
return (dot + d) >= 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(BVHNode) == 32, "BVHNode must be 32 bytes");
|
||||||
|
|
||||||
|
/// BVH Tree header in binary file
|
||||||
|
struct BVHHeader {
|
||||||
|
uint16_t nodeCount;
|
||||||
|
uint16_t triangleRefCount;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(BVHHeader) == 4, "BVHHeader must be 4 bytes");
|
||||||
|
|
||||||
|
/// Frustum planes for culling (6 planes)
|
||||||
|
struct Frustum {
|
||||||
|
struct Plane {
|
||||||
|
int32_t nx, ny, nz, d;
|
||||||
|
};
|
||||||
|
Plane planes[6];
|
||||||
|
|
||||||
|
bool testAABB(const BVHNode& node) const {
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
if (!node.testPlane(planes[i].nx, planes[i].ny, planes[i].nz, planes[i].d)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// BVH Manager - handles traversal and culling
|
||||||
|
class BVHManager {
|
||||||
|
public:
|
||||||
|
/// Initialize from separate pointers (used by splashpack loader)
|
||||||
|
void initialize(const BVHNode* nodes, uint16_t nodeCount,
|
||||||
|
const TriangleRef* triangleRefs, uint16_t triangleRefCount);
|
||||||
|
|
||||||
|
/// Traverse BVH and collect visible triangle references
|
||||||
|
/// Uses frustum culling to skip invisible branches
|
||||||
|
/// Returns number of visible triangle refs
|
||||||
|
int cullFrustum(const Frustum& frustum,
|
||||||
|
TriangleRef* outRefs,
|
||||||
|
int maxRefs) const;
|
||||||
|
|
||||||
|
/// Simpler traversal - collect all triangles in a region
|
||||||
|
/// Useful for collision queries
|
||||||
|
int queryRegion(int32_t minX, int32_t minY, int32_t minZ,
|
||||||
|
int32_t maxX, int32_t maxY, int32_t maxZ,
|
||||||
|
TriangleRef* outRefs,
|
||||||
|
int maxRefs) const;
|
||||||
|
|
||||||
|
/// Get node count
|
||||||
|
int getNodeCount() const { return m_nodeCount; }
|
||||||
|
|
||||||
|
/// Get triangle ref count
|
||||||
|
int getTriangleRefCount() const { return m_triangleRefCount; }
|
||||||
|
|
||||||
|
/// Check if BVH is loaded
|
||||||
|
bool isLoaded() const { return m_nodes != nullptr; }
|
||||||
|
|
||||||
|
void relocate(intptr_t delta) {
|
||||||
|
if (m_nodes) m_nodes = reinterpret_cast<const BVHNode*>(reinterpret_cast<intptr_t>(m_nodes) + delta);
|
||||||
|
if (m_triangleRefs) m_triangleRefs = reinterpret_cast<const TriangleRef*>(reinterpret_cast<intptr_t>(m_triangleRefs) + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const BVHNode* m_nodes = nullptr;
|
||||||
|
const TriangleRef* m_triangleRefs = nullptr;
|
||||||
|
uint16_t m_nodeCount = 0;
|
||||||
|
uint16_t m_triangleRefCount = 0;
|
||||||
|
|
||||||
|
/// Recursive frustum culling traversal
|
||||||
|
int traverseFrustum(int nodeIndex,
|
||||||
|
const Frustum& frustum,
|
||||||
|
TriangleRef* outRefs,
|
||||||
|
int currentCount,
|
||||||
|
int maxRefs) const;
|
||||||
|
|
||||||
|
/// Recursive region query traversal
|
||||||
|
int traverseRegion(int nodeIndex,
|
||||||
|
int32_t qMinX, int32_t qMinY, int32_t qMinZ,
|
||||||
|
int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ,
|
||||||
|
TriangleRef* outRefs,
|
||||||
|
int currentCount,
|
||||||
|
int maxRefs) const;
|
||||||
|
|
||||||
|
/// Test if two AABBs overlap
|
||||||
|
static bool aabbOverlap(const BVHNode& node,
|
||||||
|
int32_t qMinX, int32_t qMinY, int32_t qMinZ,
|
||||||
|
int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Room/portal data for interior scene occlusion ──
|
||||||
|
|
||||||
|
/// Per-room data loaded from splashpack v11+.
|
||||||
|
/// AABB for point-in-room tests plus a range into the room triangle-ref array.
|
||||||
|
struct RoomData {
|
||||||
|
int32_t aabbMinX, aabbMinY, aabbMinZ; // 12 bytes
|
||||||
|
int32_t aabbMaxX, aabbMaxY, aabbMaxZ; // 12 bytes
|
||||||
|
uint16_t firstTriRef; // 2 bytes - index into room tri-ref array
|
||||||
|
uint16_t triRefCount; // 2 bytes
|
||||||
|
uint32_t pad; // 4 bytes (alignment)
|
||||||
|
};
|
||||||
|
static_assert(sizeof(RoomData) == 32, "RoomData must be 32 bytes");
|
||||||
|
|
||||||
|
/// Per-portal data connecting two rooms.
|
||||||
|
/// Center position is in fixed-point world/GTE space (20.12).
|
||||||
|
/// halfW/halfH define the portal opening size.
|
||||||
|
/// Normal, right, and up define the portal's orientation in world space.
|
||||||
|
/// Corner vertices are computed as: center +/- right*halfW +/- up*halfH.
|
||||||
|
struct PortalData {
|
||||||
|
uint16_t roomA; // 2 bytes
|
||||||
|
uint16_t roomB; // 2 bytes
|
||||||
|
int32_t centerX, centerY, centerZ; // 12 bytes - portal center (20.12 fp)
|
||||||
|
int16_t halfW; // 2 bytes - half-width in GTE units (4.12 fp)
|
||||||
|
int16_t halfH; // 2 bytes - half-height in GTE units (4.12 fp)
|
||||||
|
int16_t normalX, normalY, normalZ; // 6 bytes - facing direction (4.12 fp unit vector)
|
||||||
|
int16_t pad; // 2 bytes - alignment
|
||||||
|
int16_t rightX, rightY, rightZ; // 6 bytes - local right axis (4.12 fp unit vector)
|
||||||
|
int16_t upX, upY, upZ; // 6 bytes - local up axis (4.12 fp unit vector)
|
||||||
|
};
|
||||||
|
static_assert(sizeof(PortalData) == 40, "PortalData must be 40 bytes");
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
102
src/camera.cpp
102
src/camera.cpp
@@ -9,45 +9,93 @@ psxsplash::Camera::Camera() {
|
|||||||
m_rotationMatrix = psyqo::SoftMath::generateRotationMatrix33(0, psyqo::SoftMath::Axis::X, m_trig);
|
m_rotationMatrix = psyqo::SoftMath::generateRotationMatrix33(0, psyqo::SoftMath::Axis::X, m_trig);
|
||||||
}
|
}
|
||||||
|
|
||||||
void psxsplash::Camera::moveX(psyqo::FixedPoint<12> x) { m_rotation.x += -x; }
|
void psxsplash::Camera::MoveX(psyqo::FixedPoint<12> x) { m_position.x += x; }
|
||||||
|
|
||||||
void psxsplash::Camera::moveY(psyqo::FixedPoint<12> y) { m_rotation.y += -y; }
|
void psxsplash::Camera::MoveY(psyqo::FixedPoint<12> y) { m_position.y += y; }
|
||||||
|
|
||||||
void psxsplash::Camera::moveZ(psyqo::FixedPoint<12> z) { m_rotation.z += -z; }
|
void psxsplash::Camera::MoveZ(psyqo::FixedPoint<12> z) { m_position.z += z; }
|
||||||
|
|
||||||
void psxsplash::Camera::setPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z) {
|
void psxsplash::Camera::SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z) {
|
||||||
m_rotation.x = -x;
|
m_position.x = x;
|
||||||
m_rotation.y = -y;
|
m_position.y = y;
|
||||||
m_rotation.z = -z;
|
m_position.z = z;
|
||||||
}
|
}
|
||||||
|
|
||||||
void psxsplash::Camera::rotateX(psyqo::Angle x) {
|
void psxsplash::Camera::SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z) {
|
||||||
auto rot = psyqo::SoftMath::generateRotationMatrix33(-x, psyqo::SoftMath::Axis::X, m_trig);
|
m_angleX = (int16_t)x.value;
|
||||||
|
m_angleY = (int16_t)y.value;
|
||||||
|
m_angleZ = (int16_t)z.value;
|
||||||
|
|
||||||
psyqo::SoftMath::multiplyMatrix33(m_rotationMatrix, rot, &m_rotationMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
void psxsplash::Camera::rotateY(psyqo::Angle y) {
|
|
||||||
auto rot = psyqo::SoftMath::generateRotationMatrix33(-y, psyqo::SoftMath::Axis::Y, m_trig);
|
|
||||||
|
|
||||||
psyqo::SoftMath::multiplyMatrix33(m_rotationMatrix, rot, &m_rotationMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
void psxsplash::Camera::rotateZ(psyqo::Angle z) {
|
|
||||||
auto rot = psyqo::SoftMath::generateRotationMatrix33(-z, psyqo::SoftMath::Axis::Y, m_trig);
|
|
||||||
|
|
||||||
psyqo::SoftMath::multiplyMatrix33(m_rotationMatrix, rot, &m_rotationMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
void psxsplash::Camera::setRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z) {
|
|
||||||
auto rotX = psyqo::SoftMath::generateRotationMatrix33(x, psyqo::SoftMath::Axis::X, m_trig);
|
auto rotX = psyqo::SoftMath::generateRotationMatrix33(x, psyqo::SoftMath::Axis::X, m_trig);
|
||||||
auto rotY = psyqo::SoftMath::generateRotationMatrix33(y, psyqo::SoftMath::Axis::Y, m_trig);
|
auto rotY = psyqo::SoftMath::generateRotationMatrix33(y, psyqo::SoftMath::Axis::Y, m_trig);
|
||||||
auto rotZ = psyqo::SoftMath::generateRotationMatrix33(z, psyqo::SoftMath::Axis::Z, m_trig);
|
auto rotZ = psyqo::SoftMath::generateRotationMatrix33(z, psyqo::SoftMath::Axis::Z, m_trig);
|
||||||
|
|
||||||
psyqo::SoftMath::multiplyMatrix33(rotY, rotX, &rotY);
|
psyqo::SoftMath::multiplyMatrix33(rotY, rotX, &rotY);
|
||||||
|
|
||||||
psyqo::SoftMath::multiplyMatrix33(rotY, rotZ, &rotY);
|
psyqo::SoftMath::multiplyMatrix33(rotY, rotZ, &rotY);
|
||||||
|
|
||||||
m_rotationMatrix = rotY;
|
m_rotationMatrix = rotY;
|
||||||
}
|
}
|
||||||
|
|
||||||
psyqo::Matrix33& psxsplash::Camera::getRotation() { return m_rotationMatrix; }
|
psyqo::Matrix33& psxsplash::Camera::GetRotation() { return m_rotationMatrix; }
|
||||||
|
|
||||||
|
void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
|
||||||
|
|
||||||
|
constexpr int32_t SCREEN_HALF_WIDTH = 160;
|
||||||
|
constexpr int32_t SCREEN_HALF_HEIGHT = 120;
|
||||||
|
constexpr int32_t H = 120;
|
||||||
|
|
||||||
|
int32_t rightX = m_rotationMatrix.vs[0].x.raw();
|
||||||
|
int32_t rightY = m_rotationMatrix.vs[0].y.raw();
|
||||||
|
int32_t rightZ = m_rotationMatrix.vs[0].z.raw();
|
||||||
|
|
||||||
|
int32_t upX = m_rotationMatrix.vs[1].x.raw();
|
||||||
|
int32_t upY = m_rotationMatrix.vs[1].y.raw();
|
||||||
|
int32_t upZ = m_rotationMatrix.vs[1].z.raw();
|
||||||
|
|
||||||
|
int32_t fwdX = m_rotationMatrix.vs[2].x.raw();
|
||||||
|
int32_t fwdY = m_rotationMatrix.vs[2].y.raw();
|
||||||
|
int32_t fwdZ = m_rotationMatrix.vs[2].z.raw();
|
||||||
|
|
||||||
|
int32_t camX = m_position.x.raw();
|
||||||
|
int32_t camY = m_position.y.raw();
|
||||||
|
int32_t camZ = m_position.z.raw();
|
||||||
|
|
||||||
|
frustum.planes[0].nx = fwdX;
|
||||||
|
frustum.planes[0].ny = fwdY;
|
||||||
|
frustum.planes[0].nz = fwdZ;
|
||||||
|
int64_t fwdDotCam = ((int64_t)fwdX * camX + (int64_t)fwdY * camY + (int64_t)fwdZ * camZ) >> 12;
|
||||||
|
frustum.planes[0].d = -fwdDotCam;
|
||||||
|
|
||||||
|
frustum.planes[1].nx = -fwdX;
|
||||||
|
frustum.planes[1].ny = -fwdY;
|
||||||
|
frustum.planes[1].nz = -fwdZ;
|
||||||
|
frustum.planes[1].d = fwdDotCam + (4096 * 2000);
|
||||||
|
|
||||||
|
frustum.planes[2].nx = ((int64_t)rightX * H + (int64_t)fwdX * SCREEN_HALF_WIDTH) >> 12;
|
||||||
|
frustum.planes[2].ny = ((int64_t)rightY * H + (int64_t)fwdY * SCREEN_HALF_WIDTH) >> 12;
|
||||||
|
frustum.planes[2].nz = ((int64_t)rightZ * H + (int64_t)fwdZ * SCREEN_HALF_WIDTH) >> 12;
|
||||||
|
frustum.planes[2].d = -(((int64_t)frustum.planes[2].nx * camX +
|
||||||
|
(int64_t)frustum.planes[2].ny * camY +
|
||||||
|
(int64_t)frustum.planes[2].nz * camZ) >> 12);
|
||||||
|
|
||||||
|
frustum.planes[3].nx = ((int64_t)(-rightX) * H + (int64_t)fwdX * SCREEN_HALF_WIDTH) >> 12;
|
||||||
|
frustum.planes[3].ny = ((int64_t)(-rightY) * H + (int64_t)fwdY * SCREEN_HALF_WIDTH) >> 12;
|
||||||
|
frustum.planes[3].nz = ((int64_t)(-rightZ) * H + (int64_t)fwdZ * SCREEN_HALF_WIDTH) >> 12;
|
||||||
|
frustum.planes[3].d = -(((int64_t)frustum.planes[3].nx * camX +
|
||||||
|
(int64_t)frustum.planes[3].ny * camY +
|
||||||
|
(int64_t)frustum.planes[3].nz * camZ) >> 12);
|
||||||
|
|
||||||
|
frustum.planes[4].nx = ((int64_t)upX * H + (int64_t)fwdX * SCREEN_HALF_HEIGHT) >> 12;
|
||||||
|
frustum.planes[4].ny = ((int64_t)upY * H + (int64_t)fwdY * SCREEN_HALF_HEIGHT) >> 12;
|
||||||
|
frustum.planes[4].nz = ((int64_t)upZ * H + (int64_t)fwdZ * SCREEN_HALF_HEIGHT) >> 12;
|
||||||
|
frustum.planes[4].d = -(((int64_t)frustum.planes[4].nx * camX +
|
||||||
|
(int64_t)frustum.planes[4].ny * camY +
|
||||||
|
(int64_t)frustum.planes[4].nz * camZ) >> 12);
|
||||||
|
|
||||||
|
frustum.planes[5].nx = ((int64_t)(-upX) * H + (int64_t)fwdX * SCREEN_HALF_HEIGHT) >> 12;
|
||||||
|
frustum.planes[5].ny = ((int64_t)(-upY) * H + (int64_t)fwdY * SCREEN_HALF_HEIGHT) >> 12;
|
||||||
|
frustum.planes[5].nz = ((int64_t)(-upZ) * H + (int64_t)fwdZ * SCREEN_HALF_HEIGHT) >> 12;
|
||||||
|
frustum.planes[5].d = -(((int64_t)frustum.planes[5].nx * camX +
|
||||||
|
(int64_t)frustum.planes[5].ny * camY +
|
||||||
|
(int64_t)frustum.planes[5].nz * camZ) >> 12);
|
||||||
|
}
|
||||||
@@ -1,35 +1,37 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <sys/types.h>
|
|
||||||
|
|
||||||
#include <psyqo/fixed-point.hh>
|
#include <psyqo/fixed-point.hh>
|
||||||
#include <psyqo/matrix.hh>
|
#include <psyqo/matrix.hh>
|
||||||
#include <psyqo/trigonometry.hh>
|
#include <psyqo/trigonometry.hh>
|
||||||
|
|
||||||
|
#include "bvh.hh"
|
||||||
|
|
||||||
namespace psxsplash {
|
namespace psxsplash {
|
||||||
|
|
||||||
class Camera {
|
class Camera {
|
||||||
public:
|
public:
|
||||||
Camera();
|
Camera();
|
||||||
|
|
||||||
void moveX(psyqo::FixedPoint<12> x);
|
void MoveX(psyqo::FixedPoint<12> x);
|
||||||
void moveY(psyqo::FixedPoint<12> y);
|
void MoveY(psyqo::FixedPoint<12> y);
|
||||||
void moveZ(psyqo::FixedPoint<12> y);
|
void MoveZ(psyqo::FixedPoint<12> z);
|
||||||
|
|
||||||
void setPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z);
|
void SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z);
|
||||||
|
psyqo::Vec3& GetPosition() { return m_position; }
|
||||||
|
|
||||||
psyqo::Vec3& getPosition() { return m_rotation; }
|
void SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z);
|
||||||
|
psyqo::Matrix33& GetRotation();
|
||||||
|
|
||||||
void rotateX(psyqo::Angle x);
|
void ExtractFrustum(Frustum& frustum) const;
|
||||||
void rotateY(psyqo::Angle y);
|
|
||||||
void rotateZ(psyqo::Angle z);
|
|
||||||
|
|
||||||
void setRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z);
|
int16_t GetAngleX() const { return m_angleX; }
|
||||||
psyqo::Matrix33& getRotation();
|
int16_t GetAngleY() const { return m_angleY; }
|
||||||
|
int16_t GetAngleZ() const { return m_angleZ; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
psyqo::Matrix33 m_rotationMatrix;
|
psyqo::Matrix33 m_rotationMatrix;
|
||||||
psyqo::Trig<> m_trig;
|
psyqo::Trig<> m_trig;
|
||||||
psyqo::Vec3 m_rotation;
|
psyqo::Vec3 m_position;
|
||||||
|
int16_t m_angleX = 0, m_angleY = 0, m_angleZ = 0;
|
||||||
};
|
};
|
||||||
} // namespace psxsplash
|
} // namespace psxsplash
|
||||||
89
src/cdromhelper.hh
Normal file
89
src/cdromhelper.hh
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// CDRomHelper - manages CD-ROM interrupt lifecycle around psyqo's CDRomDevice.
|
||||||
|
//
|
||||||
|
// psyqo's IRQ handler asserts if an interrupt fires with no action
|
||||||
|
// registered (m_action != nullptr check in cdrom-device.cpp:72).
|
||||||
|
// After readSectorsBlocking returns, IMask is re-enabled but no action
|
||||||
|
// is pending. We must mask before any unsolicited interrupt arrives.
|
||||||
|
//
|
||||||
|
// We mask at BOTH levels:
|
||||||
|
// - CPU level (IMask): prevents psyqo's handler from firing
|
||||||
|
// - Controller level (CauseMask=0): prevents the controller from
|
||||||
|
// generating or queueing responses, keeping its state clean
|
||||||
|
//
|
||||||
|
// The motor keeps spinning after Pause (psyqo's read action ends with
|
||||||
|
// CdlPause). This is normal PS1 behavior - PSBW and other homebrew
|
||||||
|
// leave the motor spinning too. We don't send CdlStop because handling
|
||||||
|
// its response correctly while bypassing psyqo's handler is unreliable.
|
||||||
|
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
|
||||||
|
#include <psyqo/hardware/cpu.hh>
|
||||||
|
#include <psyqo/hardware/cdrom.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
class CDRomHelper {
|
||||||
|
public:
|
||||||
|
// Call immediately after the last readSectorsBlocking returns.
|
||||||
|
// Masks interrupts at both CPU and controller level.
|
||||||
|
static void SilenceDrive() {
|
||||||
|
// Mask CPU-level IRQ so psyqo's handler can't fire.
|
||||||
|
psyqo::Hardware::CPU::IMask.clear(psyqo::Hardware::CPU::IRQ::CDRom);
|
||||||
|
psyqo::Hardware::CPU::flushWriteQueue();
|
||||||
|
|
||||||
|
// Mask controller-level interrupts. With CauseMask=0 the
|
||||||
|
// controller won't assert the interrupt line or queue
|
||||||
|
// responses. This keeps the controller in a clean state
|
||||||
|
// for the next load, unlike masking only IMask (which
|
||||||
|
// caused unacknowledged responses to pile up and eventually
|
||||||
|
// lock the controller).
|
||||||
|
psyqo::Hardware::CDRom::CauseMask = 0;
|
||||||
|
|
||||||
|
s_silenced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call before the next file load. Restores the controller to a
|
||||||
|
// clean state ready for psyqo's CDRomDevice to use.
|
||||||
|
// No-op on first scene load (nothing to restore).
|
||||||
|
static void WakeDrive() {
|
||||||
|
if (!s_silenced) return;
|
||||||
|
s_silenced = false;
|
||||||
|
|
||||||
|
// Drain any residual Cause bits while CauseMask is still 0.
|
||||||
|
drainController();
|
||||||
|
|
||||||
|
// Re-enable controller-level interrupts (INT1-5).
|
||||||
|
psyqo::Hardware::CDRom::CauseMask = 0x1f;
|
||||||
|
|
||||||
|
// Drain again: restoring CauseMask may have caused a
|
||||||
|
// pending state to be signaled.
|
||||||
|
drainController();
|
||||||
|
|
||||||
|
// Re-enable CPU-level IRQ for psyqo's handler.
|
||||||
|
psyqo::Hardware::CPU::IMask.set(psyqo::Hardware::CPU::IRQ::CDRom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static inline bool s_silenced = false;
|
||||||
|
|
||||||
|
// Acknowledge and drain any pending interrupt from the CD-ROM
|
||||||
|
// controller. Clears Cause bits, drains the response FIFO, and
|
||||||
|
// clears the CPU IReg flag.
|
||||||
|
static void drainController() {
|
||||||
|
uint8_t cause = psyqo::Hardware::CDRom::Cause;
|
||||||
|
if (cause & 7)
|
||||||
|
psyqo::Hardware::CDRom::Cause = 7;
|
||||||
|
if (cause & 0x18)
|
||||||
|
psyqo::Hardware::CDRom::Cause = 0x18;
|
||||||
|
while (psyqo::Hardware::CDRom::Ctrl.access() & 0x20)
|
||||||
|
psyqo::Hardware::CDRom::Response; // drain FIFO
|
||||||
|
psyqo::Hardware::CPU::IReg.clear(
|
||||||
|
psyqo::Hardware::CPU::IRQ::CDRom);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
|
|
||||||
|
#endif // LOADER_CDROM
|
||||||
308
src/collision.cpp
Normal file
308
src/collision.cpp
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
#include "collision.hh"
|
||||||
|
#include "scenemanager.hh"
|
||||||
|
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
|
||||||
|
using FP = psyqo::FixedPoint<12>;
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
psyqo::FixedPoint<12> SpatialGrid::WORLD_MIN = FP(-16);
|
||||||
|
psyqo::FixedPoint<12> SpatialGrid::WORLD_MAX = FP(16);
|
||||||
|
psyqo::FixedPoint<12> SpatialGrid::CELL_SIZE = FP(4);
|
||||||
|
|
||||||
|
void AABB::expand(const psyqo::Vec3& delta) {
|
||||||
|
psyqo::FixedPoint<12> zero;
|
||||||
|
if (delta.x > zero) max.x = max.x + delta.x;
|
||||||
|
else min.x = min.x + delta.x;
|
||||||
|
if (delta.y > zero) max.y = max.y + delta.y;
|
||||||
|
else min.y = min.y + delta.y;
|
||||||
|
if (delta.z > zero) max.z = max.z + delta.z;
|
||||||
|
else min.z = min.z + delta.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SpatialGrid Implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void SpatialGrid::clear() {
|
||||||
|
for (int i = 0; i < CELL_COUNT; i++) {
|
||||||
|
m_cells[i].count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const {
|
||||||
|
auto px = pos.x;
|
||||||
|
auto py = pos.y;
|
||||||
|
auto pz = pos.z;
|
||||||
|
|
||||||
|
if (px < WORLD_MIN) px = WORLD_MIN;
|
||||||
|
if (px > WORLD_MAX) px = WORLD_MAX;
|
||||||
|
if (py < WORLD_MIN) py = WORLD_MIN;
|
||||||
|
if (py > WORLD_MAX) py = WORLD_MAX;
|
||||||
|
if (pz < WORLD_MIN) pz = WORLD_MIN;
|
||||||
|
if (pz > WORLD_MAX) pz = WORLD_MAX;
|
||||||
|
|
||||||
|
gx = ((px - WORLD_MIN) / CELL_SIZE).integer();
|
||||||
|
gy = ((py - WORLD_MIN) / CELL_SIZE).integer();
|
||||||
|
gz = ((pz - WORLD_MIN) / CELL_SIZE).integer();
|
||||||
|
|
||||||
|
if (gx < 0) gx = 0;
|
||||||
|
if (gx >= GRID_SIZE) gx = GRID_SIZE - 1;
|
||||||
|
if (gy < 0) gy = 0;
|
||||||
|
if (gy >= GRID_SIZE) gy = GRID_SIZE - 1;
|
||||||
|
if (gz < 0) gz = 0;
|
||||||
|
if (gz >= GRID_SIZE) gz = GRID_SIZE - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) {
|
||||||
|
int minGx, minGy, minGz;
|
||||||
|
int maxGx, maxGy, maxGz;
|
||||||
|
|
||||||
|
worldToGrid(bounds.min, minGx, minGy, minGz);
|
||||||
|
worldToGrid(bounds.max, maxGx, maxGy, maxGz);
|
||||||
|
|
||||||
|
for (int gz = minGz; gz <= maxGz; gz++) {
|
||||||
|
for (int gy = minGy; gy <= maxGy; gy++) {
|
||||||
|
for (int gx = minGx; gx <= maxGx; gx++) {
|
||||||
|
int cellIndex = gx + gy * GRID_SIZE + gz * GRID_SIZE * GRID_SIZE;
|
||||||
|
Cell& cell = m_cells[cellIndex];
|
||||||
|
|
||||||
|
if (cell.count < MAX_OBJECTS_PER_CELL) {
|
||||||
|
cell.objectIndices[cell.count++] = objectIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const {
|
||||||
|
int resultCount = 0;
|
||||||
|
|
||||||
|
int minGx, minGy, minGz;
|
||||||
|
int maxGx, maxGy, maxGz;
|
||||||
|
|
||||||
|
worldToGrid(bounds.min, minGx, minGy, minGz);
|
||||||
|
worldToGrid(bounds.max, maxGx, maxGy, maxGz);
|
||||||
|
|
||||||
|
uint32_t addedMaskLow = 0;
|
||||||
|
uint32_t addedMaskHigh = 0;
|
||||||
|
|
||||||
|
for (int gz = minGz; gz <= maxGz; gz++) {
|
||||||
|
for (int gy = minGy; gy <= maxGy; gy++) {
|
||||||
|
for (int gx = minGx; gx <= maxGx; gx++) {
|
||||||
|
int cellIndex = gx + gy * GRID_SIZE + gz * GRID_SIZE * GRID_SIZE;
|
||||||
|
const Cell& cell = m_cells[cellIndex];
|
||||||
|
|
||||||
|
for (int i = 0; i < cell.count; i++) {
|
||||||
|
uint16_t objIndex = cell.objectIndices[i];
|
||||||
|
|
||||||
|
if (objIndex < 32) {
|
||||||
|
uint32_t bit = 1U << objIndex;
|
||||||
|
if (addedMaskLow & bit) continue;
|
||||||
|
addedMaskLow |= bit;
|
||||||
|
} else if (objIndex < 64) {
|
||||||
|
uint32_t bit = 1U << (objIndex - 32);
|
||||||
|
if (addedMaskHigh & bit) continue;
|
||||||
|
addedMaskHigh |= bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultCount < maxResults) {
|
||||||
|
output[resultCount++] = objIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CollisionSystem Implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void CollisionSystem::init() {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollisionSystem::reset() {
|
||||||
|
m_colliderCount = 0;
|
||||||
|
m_triggerBoxCount = 0;
|
||||||
|
m_resultCount = 0;
|
||||||
|
m_triggerPairCount = 0;
|
||||||
|
m_grid.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollisionSystem::registerCollider(uint16_t gameObjectIndex, const AABB& localBounds,
|
||||||
|
CollisionType type, CollisionMask mask) {
|
||||||
|
if (m_colliderCount >= MAX_COLLIDERS) return;
|
||||||
|
|
||||||
|
CollisionData& data = m_colliders[m_colliderCount++];
|
||||||
|
data.localBounds = localBounds;
|
||||||
|
data.bounds = localBounds;
|
||||||
|
data.type = type;
|
||||||
|
data.layerMask = mask;
|
||||||
|
data.gameObjectIndex = gameObjectIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollisionSystem::registerTriggerBox(const AABB& bounds, int16_t luaFileIndex) {
|
||||||
|
if (m_triggerBoxCount >= MAX_TRIGGER_BOXES) return;
|
||||||
|
|
||||||
|
TriggerBoxData& tb = m_triggerBoxes[m_triggerBoxCount++];
|
||||||
|
tb.bounds = bounds;
|
||||||
|
tb.luaFileIndex = luaFileIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollisionSystem::updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position,
|
||||||
|
const psyqo::Matrix33& rotation) {
|
||||||
|
for (int i = 0; i < m_colliderCount; i++) {
|
||||||
|
if (m_colliders[i].gameObjectIndex == gameObjectIndex) {
|
||||||
|
m_colliders[i].bounds.min = m_colliders[i].localBounds.min + position;
|
||||||
|
m_colliders[i].bounds.max = m_colliders[i].localBounds.max + position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int CollisionSystem::detectCollisions(const AABB& playerAABB, psyqo::Vec3& pushBack, SceneManager& scene) {
|
||||||
|
m_resultCount = 0;
|
||||||
|
const FP zero(0);
|
||||||
|
pushBack = psyqo::Vec3{zero, zero, zero};
|
||||||
|
|
||||||
|
// Rebuild spatial grid with active colliders only
|
||||||
|
m_grid.clear();
|
||||||
|
for (int i = 0; i < m_colliderCount; i++) {
|
||||||
|
auto* go = scene.getGameObject(m_colliders[i].gameObjectIndex);
|
||||||
|
if (go && go->isActive()) {
|
||||||
|
m_grid.insert(i, m_colliders[i].bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test player AABB against all colliders for push-back
|
||||||
|
uint16_t nearby[32];
|
||||||
|
int nearbyCount = m_grid.queryAABB(playerAABB, nearby, 32);
|
||||||
|
|
||||||
|
for (int j = 0; j < nearbyCount; j++) {
|
||||||
|
int idx = nearby[j];
|
||||||
|
const CollisionData& collider = m_colliders[idx];
|
||||||
|
if (collider.type == CollisionType::None) continue;
|
||||||
|
|
||||||
|
psyqo::Vec3 normal;
|
||||||
|
psyqo::FixedPoint<12> penetration;
|
||||||
|
|
||||||
|
if (testAABB(playerAABB, collider.bounds, normal, penetration)) {
|
||||||
|
// Accumulate push-back along the separation normal
|
||||||
|
pushBack.x = pushBack.x + normal.x * penetration;
|
||||||
|
pushBack.y = pushBack.y + normal.y * penetration;
|
||||||
|
pushBack.z = pushBack.z + normal.z * penetration;
|
||||||
|
|
||||||
|
if (m_resultCount < MAX_COLLISION_RESULTS) {
|
||||||
|
CollisionResult& result = m_results[m_resultCount++];
|
||||||
|
result.objectA = 0xFFFF; // player
|
||||||
|
result.objectB = collider.gameObjectIndex;
|
||||||
|
result.normal = normal;
|
||||||
|
result.penetration = penetration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_resultCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollisionSystem::detectTriggers(const AABB& playerAABB, SceneManager& scene) {
|
||||||
|
int writeIndex = 0;
|
||||||
|
|
||||||
|
// Mark all existing pairs as potentially stale
|
||||||
|
for (int i = 0; i < m_triggerPairCount; i++) {
|
||||||
|
m_triggerPairs[i].framesSinceContact++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test player against each trigger box
|
||||||
|
for (int ti = 0; ti < m_triggerBoxCount; ti++) {
|
||||||
|
const TriggerBoxData& tb = m_triggerBoxes[ti];
|
||||||
|
|
||||||
|
if (!playerAABB.intersects(tb.bounds)) continue;
|
||||||
|
|
||||||
|
// Find existing pair
|
||||||
|
bool found = false;
|
||||||
|
for (int pi = 0; pi < m_triggerPairCount; pi++) {
|
||||||
|
if (m_triggerPairs[pi].triggerIndex == ti) {
|
||||||
|
m_triggerPairs[pi].framesSinceContact = 0;
|
||||||
|
if (m_triggerPairs[pi].state == 0) {
|
||||||
|
m_triggerPairs[pi].state = 1; // enter -> active
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New pair: enter
|
||||||
|
if (!found && m_triggerPairCount < MAX_TRIGGER_PAIRS) {
|
||||||
|
TriggerPair& pair = m_triggerPairs[m_triggerPairCount++];
|
||||||
|
pair.triggerIndex = ti;
|
||||||
|
pair.padding = 0;
|
||||||
|
pair.framesSinceContact = 0;
|
||||||
|
pair.state = 0;
|
||||||
|
pair.padding2 = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process pairs: fire events and clean up exited pairs
|
||||||
|
writeIndex = 0;
|
||||||
|
for (int i = 0; i < m_triggerPairCount; i++) {
|
||||||
|
TriggerPair& pair = m_triggerPairs[i];
|
||||||
|
int16_t luaIdx = m_triggerBoxes[pair.triggerIndex].luaFileIndex;
|
||||||
|
|
||||||
|
if (pair.state == 0) {
|
||||||
|
// Enter
|
||||||
|
scene.fireTriggerEnter(luaIdx, pair.triggerIndex);
|
||||||
|
pair.state = 1;
|
||||||
|
m_triggerPairs[writeIndex++] = pair;
|
||||||
|
} else if (pair.framesSinceContact > 2) {
|
||||||
|
// Exit
|
||||||
|
scene.fireTriggerExit(luaIdx, pair.triggerIndex);
|
||||||
|
} else {
|
||||||
|
// Still inside, keep alive
|
||||||
|
m_triggerPairs[writeIndex++] = pair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_triggerPairCount = writeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
|
||||||
|
psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const {
|
||||||
|
if (a.max.x < b.min.x || a.min.x > b.max.x) return false;
|
||||||
|
if (a.max.y < b.min.y || a.min.y > b.max.y) return false;
|
||||||
|
if (a.max.z < b.min.z || a.min.z > b.max.z) return false;
|
||||||
|
|
||||||
|
auto overlapX1 = a.max.x - b.min.x;
|
||||||
|
auto overlapX2 = b.max.x - a.min.x;
|
||||||
|
auto overlapY1 = a.max.y - b.min.y;
|
||||||
|
auto overlapY2 = b.max.y - a.min.y;
|
||||||
|
auto overlapZ1 = a.max.z - b.min.z;
|
||||||
|
auto overlapZ2 = b.max.z - a.min.z;
|
||||||
|
|
||||||
|
auto minOverlapX = (overlapX1 < overlapX2) ? overlapX1 : overlapX2;
|
||||||
|
auto minOverlapY = (overlapY1 < overlapY2) ? overlapY1 : overlapY2;
|
||||||
|
auto minOverlapZ = (overlapZ1 < overlapZ2) ? overlapZ1 : overlapZ2;
|
||||||
|
|
||||||
|
const FP zero(0);
|
||||||
|
const FP one(1);
|
||||||
|
const FP negOne(-1);
|
||||||
|
|
||||||
|
if (minOverlapX <= minOverlapY && minOverlapX <= minOverlapZ) {
|
||||||
|
penetration = minOverlapX;
|
||||||
|
normal = psyqo::Vec3{(overlapX1 < overlapX2) ? negOne : one, zero, zero};
|
||||||
|
} else if (minOverlapY <= minOverlapZ) {
|
||||||
|
penetration = minOverlapY;
|
||||||
|
normal = psyqo::Vec3{zero, (overlapY1 < overlapY2) ? negOne : one, zero};
|
||||||
|
} else {
|
||||||
|
penetration = minOverlapZ;
|
||||||
|
normal = psyqo::Vec3{zero, zero, (overlapZ1 < overlapZ2) ? negOne : one};
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
141
src/collision.hh
Normal file
141
src/collision.hh
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
#include <EASTL/vector.h>
|
||||||
|
|
||||||
|
#include "gameobject.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
class SceneManager;
|
||||||
|
|
||||||
|
enum class CollisionType : uint8_t {
|
||||||
|
None = 0,
|
||||||
|
Solid = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
using CollisionMask = uint8_t;
|
||||||
|
|
||||||
|
struct AABB {
|
||||||
|
psyqo::Vec3 min;
|
||||||
|
psyqo::Vec3 max;
|
||||||
|
|
||||||
|
bool intersects(const AABB& other) const {
|
||||||
|
return (min.x <= other.max.x && max.x >= other.min.x) &&
|
||||||
|
(min.y <= other.max.y && max.y >= other.min.y) &&
|
||||||
|
(min.z <= other.max.z && max.z >= other.min.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool contains(const psyqo::Vec3& point) const {
|
||||||
|
return (point.x >= min.x && point.x <= max.x) &&
|
||||||
|
(point.y >= min.y && point.y <= max.y) &&
|
||||||
|
(point.z >= min.z && point.z <= max.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
void expand(const psyqo::Vec3& delta);
|
||||||
|
};
|
||||||
|
static_assert(sizeof(AABB) == 24);
|
||||||
|
|
||||||
|
struct CollisionData {
|
||||||
|
AABB localBounds;
|
||||||
|
AABB bounds;
|
||||||
|
CollisionType type;
|
||||||
|
CollisionMask layerMask;
|
||||||
|
uint16_t gameObjectIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CollisionResult {
|
||||||
|
uint16_t objectA;
|
||||||
|
uint16_t objectB;
|
||||||
|
psyqo::Vec3 normal;
|
||||||
|
psyqo::FixedPoint<12> penetration;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TriggerBoxData {
|
||||||
|
AABB bounds;
|
||||||
|
int16_t luaFileIndex;
|
||||||
|
uint16_t padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TriggerPair {
|
||||||
|
uint16_t triggerIndex;
|
||||||
|
uint16_t padding;
|
||||||
|
uint8_t framesSinceContact;
|
||||||
|
uint8_t state; // 0=new(enter), 1=active, 2=exiting
|
||||||
|
uint16_t padding2;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SpatialGrid {
|
||||||
|
public:
|
||||||
|
static constexpr int GRID_SIZE = 8;
|
||||||
|
static constexpr int CELL_COUNT = GRID_SIZE * GRID_SIZE * GRID_SIZE;
|
||||||
|
static constexpr int MAX_OBJECTS_PER_CELL = 16;
|
||||||
|
|
||||||
|
static psyqo::FixedPoint<12> WORLD_MIN;
|
||||||
|
static psyqo::FixedPoint<12> WORLD_MAX;
|
||||||
|
static psyqo::FixedPoint<12> CELL_SIZE;
|
||||||
|
|
||||||
|
struct Cell {
|
||||||
|
uint16_t objectIndices[MAX_OBJECTS_PER_CELL];
|
||||||
|
uint8_t count;
|
||||||
|
uint8_t padding[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
void insert(uint16_t objectIndex, const AABB& bounds);
|
||||||
|
int queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Cell m_cells[CELL_COUNT];
|
||||||
|
void worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollisionSystem {
|
||||||
|
public:
|
||||||
|
static constexpr int MAX_COLLIDERS = 64;
|
||||||
|
static constexpr int MAX_TRIGGER_BOXES = 32;
|
||||||
|
static constexpr int MAX_TRIGGER_PAIRS = 32;
|
||||||
|
static constexpr int MAX_COLLISION_RESULTS = 32;
|
||||||
|
|
||||||
|
CollisionSystem() = default;
|
||||||
|
|
||||||
|
void init();
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
void registerCollider(uint16_t gameObjectIndex, const AABB& localBounds,
|
||||||
|
CollisionType type, CollisionMask mask);
|
||||||
|
void updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position,
|
||||||
|
const psyqo::Matrix33& rotation);
|
||||||
|
|
||||||
|
void registerTriggerBox(const AABB& bounds, int16_t luaFileIndex);
|
||||||
|
|
||||||
|
int detectCollisions(const AABB& playerAABB, psyqo::Vec3& pushBack, class SceneManager& scene);
|
||||||
|
|
||||||
|
void detectTriggers(const AABB& playerAABB, class SceneManager& scene);
|
||||||
|
|
||||||
|
const CollisionResult* getResults() const { return m_results; }
|
||||||
|
int getResultCount() const { return m_resultCount; }
|
||||||
|
|
||||||
|
int getColliderCount() const { return m_colliderCount; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
CollisionData m_colliders[MAX_COLLIDERS];
|
||||||
|
int m_colliderCount = 0;
|
||||||
|
|
||||||
|
TriggerBoxData m_triggerBoxes[MAX_TRIGGER_BOXES];
|
||||||
|
int m_triggerBoxCount = 0;
|
||||||
|
|
||||||
|
SpatialGrid m_grid;
|
||||||
|
|
||||||
|
CollisionResult m_results[MAX_COLLISION_RESULTS];
|
||||||
|
int m_resultCount = 0;
|
||||||
|
|
||||||
|
TriggerPair m_triggerPairs[MAX_TRIGGER_PAIRS];
|
||||||
|
int m_triggerPairCount = 0;
|
||||||
|
|
||||||
|
bool testAABB(const AABB& a, const AABB& b,
|
||||||
|
psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
233
src/controls.cpp
Normal file
233
src/controls.cpp
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
#include "controls.hh"
|
||||||
|
|
||||||
|
#include <psyqo/hardware/cpu.hh>
|
||||||
|
#include <psyqo/hardware/sio.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace psyqo::Hardware;
|
||||||
|
|
||||||
|
void busyLoop(unsigned delay) {
|
||||||
|
unsigned cycles = 0;
|
||||||
|
while (++cycles < delay) asm("");
|
||||||
|
}
|
||||||
|
|
||||||
|
void flushRxBuffer() {
|
||||||
|
while (SIO::Stat & SIO::Status::STAT_RXRDY) {
|
||||||
|
SIO::Data.throwAway();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t transceive(uint8_t dataOut) {
|
||||||
|
SIO::Ctrl |= SIO::Control::CTRL_ERRRES;
|
||||||
|
CPU::IReg.clear(CPU::IRQ::Controller);
|
||||||
|
SIO::Data = dataOut;
|
||||||
|
while (!(SIO::Stat & SIO::Status::STAT_RXRDY));
|
||||||
|
return SIO::Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool waitForAck() {
|
||||||
|
int cyclesWaited = 0;
|
||||||
|
static constexpr int ackTimeout = 0x137;
|
||||||
|
while (!(CPU::IReg.isSet(CPU::IRQ::Controller)) && ++cyclesWaited < ackTimeout);
|
||||||
|
if (cyclesWaited >= ackTimeout) return false;
|
||||||
|
while (SIO::Stat & SIO::Status::STAT_ACK); // Wait for ACK to go high
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void configurePort(uint8_t port) {
|
||||||
|
SIO::Ctrl = (port * SIO::Control::CTRL_PORTSEL) | SIO::Control::CTRL_DTR;
|
||||||
|
SIO::Baud = 0x88;
|
||||||
|
flushRxBuffer();
|
||||||
|
SIO::Ctrl |= (SIO::Control::CTRL_TXEN | SIO::Control::CTRL_ACKIRQEN);
|
||||||
|
busyLoop(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a command sequence to the pad and wait for ACK between each byte.
|
||||||
|
// Returns false if ACK was lost at any point.
|
||||||
|
bool sendCommand(const uint8_t *cmd, unsigned len) {
|
||||||
|
for (unsigned i = 0; i < len; i++) {
|
||||||
|
transceive(cmd[i]);
|
||||||
|
if (i < len - 1) {
|
||||||
|
if (!waitForAck()) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void psxsplash::Controls::forceAnalogMode() {
|
||||||
|
// Initialize SIO for pad communication
|
||||||
|
using namespace psyqo::Hardware;
|
||||||
|
SIO::Ctrl = SIO::Control::CTRL_IR;
|
||||||
|
SIO::Baud = 0x88;
|
||||||
|
SIO::Mode = 0xd;
|
||||||
|
SIO::Ctrl = 0;
|
||||||
|
|
||||||
|
// Sequence for port 0 (Pad 1):
|
||||||
|
// 1) Enter config mode
|
||||||
|
static const uint8_t enterConfig[] = {0x01, 0x43, 0x00, 0x01, 0x00};
|
||||||
|
// 2) Set analog mode (0x01) + lock (0x03)
|
||||||
|
static const uint8_t setAnalog[] = {0x01, 0x44, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
// 3) Exit config mode
|
||||||
|
static const uint8_t exitConfig[] = {0x01, 0x43, 0x00, 0x00, 0x00};
|
||||||
|
|
||||||
|
configurePort(0);
|
||||||
|
sendCommand(enterConfig, sizeof(enterConfig));
|
||||||
|
SIO::Ctrl = 0;
|
||||||
|
|
||||||
|
configurePort(0);
|
||||||
|
sendCommand(setAnalog, sizeof(setAnalog));
|
||||||
|
SIO::Ctrl = 0;
|
||||||
|
|
||||||
|
configurePort(0);
|
||||||
|
sendCommand(exitConfig, sizeof(exitConfig));
|
||||||
|
SIO::Ctrl = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Controls::Init() { m_input.initialize(); }
|
||||||
|
|
||||||
|
bool psxsplash::Controls::isDigitalPad() const {
|
||||||
|
uint8_t padType = m_input.getPadType(psyqo::AdvancedPad::Pad::Pad1a);
|
||||||
|
// Digital pad (0x41) has no analog sticks
|
||||||
|
// Also treat disconnected pads as digital (D-pad still works through button API)
|
||||||
|
return padType == psyqo::AdvancedPad::PadType::DigitalPad ||
|
||||||
|
padType == psyqo::AdvancedPad::PadType::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Controls::getDpadAxes(int16_t &outX, int16_t &outY) const {
|
||||||
|
outX = 0;
|
||||||
|
outY = 0;
|
||||||
|
// D-pad produces full-magnitude values (like pushing the stick to the edge)
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Up))
|
||||||
|
outY = -127;
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Down))
|
||||||
|
outY = 127;
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Left))
|
||||||
|
outX = -127;
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Right))
|
||||||
|
outX = 127;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Controls::UpdateButtonStates() {
|
||||||
|
m_previousButtons = m_currentButtons;
|
||||||
|
|
||||||
|
// Read all button states into a single bitmask
|
||||||
|
m_currentButtons = 0;
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Cross)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Cross);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Circle)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Circle);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Square)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Square);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Triangle)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Triangle);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::L1);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L2)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::L2);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L3)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::L3);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::R1);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R2)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::R2);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R3)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::R3);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Start)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Start);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Select)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Select);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Up)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Up);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Down)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Down);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Left)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Left);
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Right)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Right);
|
||||||
|
|
||||||
|
// Calculate pressed and released buttons
|
||||||
|
m_buttonsPressed = m_currentButtons & ~m_previousButtons;
|
||||||
|
m_buttonsReleased = m_previousButtons & ~m_currentButtons;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Controls::HandleControls(psyqo::Vec3 &playerPosition, psyqo::Angle &playerRotationX,
|
||||||
|
psyqo::Angle &playerRotationY, psyqo::Angle &playerRotationZ, bool freecam,
|
||||||
|
int deltaFrames) {
|
||||||
|
bool digital = isDigitalPad();
|
||||||
|
|
||||||
|
int16_t rightXOffset, rightYOffset, leftXOffset, leftYOffset;
|
||||||
|
|
||||||
|
if (digital) {
|
||||||
|
// Digital pad: use D-pad for movement, L1/R1 for rotation
|
||||||
|
getDpadAxes(leftXOffset, leftYOffset);
|
||||||
|
// L1/R1 for horizontal look rotation (no vertical on digital)
|
||||||
|
rightXOffset = 0;
|
||||||
|
rightYOffset = 0;
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1))
|
||||||
|
rightXOffset = 90;
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1))
|
||||||
|
rightXOffset = -90;
|
||||||
|
} else {
|
||||||
|
// Analog pad: read stick ADC values
|
||||||
|
uint8_t rightX = m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 0);
|
||||||
|
uint8_t rightY = m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 1);
|
||||||
|
uint8_t leftX = m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 2);
|
||||||
|
uint8_t leftY = m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 3);
|
||||||
|
|
||||||
|
rightXOffset = (int16_t)rightX - 0x80;
|
||||||
|
rightYOffset = (int16_t)rightY - 0x80;
|
||||||
|
leftXOffset = (int16_t)leftX - 0x80;
|
||||||
|
leftYOffset = (int16_t)leftY - 0x80;
|
||||||
|
|
||||||
|
// On analog pad, also check D-pad as fallback (when sticks are centered)
|
||||||
|
if (__builtin_abs(leftXOffset) < m_stickDeadzone && __builtin_abs(leftYOffset) < m_stickDeadzone) {
|
||||||
|
int16_t dpadX, dpadY;
|
||||||
|
getDpadAxes(dpadX, dpadY);
|
||||||
|
if (dpadX != 0 || dpadY != 0) {
|
||||||
|
leftXOffset = dpadX;
|
||||||
|
leftYOffset = dpadY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint toggle (L3 for analog, Square for digital)
|
||||||
|
if (__builtin_abs(leftXOffset) < m_stickDeadzone && __builtin_abs(leftYOffset) < m_stickDeadzone) {
|
||||||
|
m_sprinting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store final stick values for Lua API access
|
||||||
|
m_leftStickX = leftXOffset;
|
||||||
|
m_leftStickY = leftYOffset;
|
||||||
|
m_rightStickX = rightXOffset;
|
||||||
|
m_rightStickY = rightYOffset;
|
||||||
|
|
||||||
|
if (digital) {
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Square)) {
|
||||||
|
m_sprinting = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L3)) {
|
||||||
|
m_sprinting = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
psyqo::FixedPoint<12> speed = m_sprinting ? m_sprintSpeed : m_moveSpeed;
|
||||||
|
|
||||||
|
// Rotation (right stick or L1/R1)
|
||||||
|
if (__builtin_abs(rightXOffset) > m_stickDeadzone) {
|
||||||
|
playerRotationY += (rightXOffset * rotSpeed * deltaFrames) >> 7;
|
||||||
|
}
|
||||||
|
if (__builtin_abs(rightYOffset) > m_stickDeadzone) {
|
||||||
|
playerRotationX -= (rightYOffset * rotSpeed * deltaFrames) >> 7;
|
||||||
|
playerRotationX = eastl::clamp(playerRotationX, -0.5_pi, 0.5_pi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movement (left stick or D-pad)
|
||||||
|
if (__builtin_abs(leftYOffset) > m_stickDeadzone) {
|
||||||
|
psyqo::FixedPoint<12> forward = -(leftYOffset * speed * deltaFrames) >> 7;
|
||||||
|
playerPosition.x += m_trig.sin(playerRotationY) * forward;
|
||||||
|
playerPosition.z += m_trig.cos(playerRotationY) * forward;
|
||||||
|
}
|
||||||
|
if (__builtin_abs(leftXOffset) > m_stickDeadzone) {
|
||||||
|
psyqo::FixedPoint<12> strafe = -(leftXOffset * speed * deltaFrames) >> 7;
|
||||||
|
playerPosition.x -= m_trig.cos(playerRotationY) * strafe;
|
||||||
|
playerPosition.z += m_trig.sin(playerRotationY) * strafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freecam) {
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1)) {
|
||||||
|
playerPosition.y += speed * deltaFrames;
|
||||||
|
}
|
||||||
|
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1)) {
|
||||||
|
playerPosition.y -= speed * deltaFrames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/controls.hh
Normal file
90
src/controls.hh
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <psyqo/advancedpad.hh>
|
||||||
|
#include <psyqo/trigonometry.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
using namespace psyqo::fixed_point_literals;
|
||||||
|
using namespace psyqo::trig_literals;
|
||||||
|
|
||||||
|
class Controls {
|
||||||
|
public:
|
||||||
|
/// Force DualShock into analog mode
|
||||||
|
/// Must be called BEFORE Init() since Init() hands SIO control to AdvancedPad.
|
||||||
|
void forceAnalogMode();
|
||||||
|
|
||||||
|
void Init();
|
||||||
|
void HandleControls(psyqo::Vec3 &playerPosition, psyqo::Angle &playerRotationX, psyqo::Angle &playerRotationY,
|
||||||
|
psyqo::Angle &playerRotationZ, bool freecam, int deltaFrames);
|
||||||
|
|
||||||
|
/// Update button state tracking - call before HandleControls
|
||||||
|
void UpdateButtonStates();
|
||||||
|
|
||||||
|
/// Set movement speeds from splashpack data (call once after scene load)
|
||||||
|
void setMoveSpeed(psyqo::FixedPoint<12, uint16_t> speed) { m_moveSpeed.value = speed.value; }
|
||||||
|
void setSprintSpeed(psyqo::FixedPoint<12, uint16_t> speed) { m_sprintSpeed.value = speed.value; }
|
||||||
|
|
||||||
|
/// Check if a button was just pressed this frame
|
||||||
|
bool wasButtonPressed(psyqo::AdvancedPad::Button button) const {
|
||||||
|
uint16_t mask = 1u << static_cast<uint16_t>(button);
|
||||||
|
return (m_currentButtons & mask) && !(m_previousButtons & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a button was just released this frame
|
||||||
|
bool wasButtonReleased(psyqo::AdvancedPad::Button button) const {
|
||||||
|
uint16_t mask = 1u << static_cast<uint16_t>(button);
|
||||||
|
return !(m_currentButtons & mask) && (m_previousButtons & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a button is currently held
|
||||||
|
bool isButtonHeld(psyqo::AdvancedPad::Button button) const {
|
||||||
|
return m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get bitmask of buttons pressed this frame
|
||||||
|
uint16_t getButtonsPressed() const { return m_buttonsPressed; }
|
||||||
|
|
||||||
|
/// Get bitmask of buttons released this frame
|
||||||
|
uint16_t getButtonsReleased() const { return m_buttonsReleased; }
|
||||||
|
|
||||||
|
/// Analog stick accessors (set during HandleControls)
|
||||||
|
int16_t getLeftStickX() const { return m_leftStickX; }
|
||||||
|
int16_t getLeftStickY() const { return m_leftStickY; }
|
||||||
|
int16_t getRightStickX() const { return m_rightStickX; }
|
||||||
|
int16_t getRightStickY() const { return m_rightStickY; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
psyqo::AdvancedPad m_input;
|
||||||
|
psyqo::Trig<> m_trig;
|
||||||
|
|
||||||
|
bool m_sprinting = false;
|
||||||
|
static constexpr uint8_t m_stickDeadzone = 0x30;
|
||||||
|
static constexpr psyqo::Angle rotSpeed = 0.01_pi;
|
||||||
|
|
||||||
|
// Configurable movement speeds (set from splashpack, or defaults)
|
||||||
|
psyqo::FixedPoint<12> m_moveSpeed = 0.002_fp;
|
||||||
|
psyqo::FixedPoint<12> m_sprintSpeed = 0.01_fp;
|
||||||
|
|
||||||
|
// Button state tracking
|
||||||
|
uint16_t m_previousButtons = 0;
|
||||||
|
uint16_t m_currentButtons = 0;
|
||||||
|
uint16_t m_buttonsPressed = 0;
|
||||||
|
uint16_t m_buttonsReleased = 0;
|
||||||
|
|
||||||
|
// Analog stick values (centered at 0, range -127 to +127)
|
||||||
|
int16_t m_leftStickX = 0;
|
||||||
|
int16_t m_leftStickY = 0;
|
||||||
|
int16_t m_rightStickX = 0;
|
||||||
|
int16_t m_rightStickY = 0;
|
||||||
|
|
||||||
|
/// Returns true if the connected pad is digital-only (no analog sticks)
|
||||||
|
bool isDigitalPad() const;
|
||||||
|
|
||||||
|
/// Get movement axes from D-pad as simulated stick values (-127 to +127)
|
||||||
|
void getDpadAxes(int16_t &outX, int16_t &outY) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
305
src/cutscene.cpp
Normal file
305
src/cutscene.cpp
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
#include "cutscene.hh"
|
||||||
|
#include "interpolation.hh"
|
||||||
|
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/soft-math.hh>
|
||||||
|
#include <psyqo/trigonometry.hh>
|
||||||
|
#include "streq.hh"
|
||||||
|
#include "uisystem.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio,
|
||||||
|
UISystem* uiSystem) {
|
||||||
|
m_cutscenes = cutscenes;
|
||||||
|
m_count = count;
|
||||||
|
m_active = nullptr;
|
||||||
|
m_frame = 0;
|
||||||
|
m_nextAudio = 0;
|
||||||
|
m_loop = false;
|
||||||
|
m_camera = camera;
|
||||||
|
m_audio = audio;
|
||||||
|
m_uiSystem = uiSystem;
|
||||||
|
m_onCompleteRef = LUA_NOREF;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CutscenePlayer::play(const char* name, bool loop) {
|
||||||
|
if (!name || !m_cutscenes) return false;
|
||||||
|
m_loop = loop;
|
||||||
|
|
||||||
|
for (int i = 0; i < m_count; i++) {
|
||||||
|
if (m_cutscenes[i].name && streq(m_cutscenes[i].name, name)) {
|
||||||
|
m_active = &m_cutscenes[i];
|
||||||
|
m_frame = 0;
|
||||||
|
m_nextAudio = 0;
|
||||||
|
|
||||||
|
// Capture initial state for pre-first-keyframe blending
|
||||||
|
for (uint8_t ti = 0; ti < m_active->trackCount; ti++) {
|
||||||
|
CutsceneTrack& track = m_active->tracks[ti];
|
||||||
|
track.initialValues[0] = track.initialValues[1] = track.initialValues[2] = 0;
|
||||||
|
switch (track.trackType) {
|
||||||
|
case TrackType::CameraPosition:
|
||||||
|
if (m_camera) {
|
||||||
|
auto& pos = m_camera->GetPosition();
|
||||||
|
track.initialValues[0] = (int16_t)pos.x.value;
|
||||||
|
track.initialValues[1] = (int16_t)pos.y.value;
|
||||||
|
track.initialValues[2] = (int16_t)pos.z.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TrackType::CameraRotation:
|
||||||
|
if (m_camera) {
|
||||||
|
track.initialValues[0] = m_camera->GetAngleX();
|
||||||
|
track.initialValues[1] = m_camera->GetAngleY();
|
||||||
|
track.initialValues[2] = m_camera->GetAngleZ();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TrackType::ObjectPosition:
|
||||||
|
if (track.target) {
|
||||||
|
track.initialValues[0] = (int16_t)track.target->position.x.value;
|
||||||
|
track.initialValues[1] = (int16_t)track.target->position.y.value;
|
||||||
|
track.initialValues[2] = (int16_t)track.target->position.z.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TrackType::ObjectRotation:
|
||||||
|
// Initial rotation angles: 0,0,0 (no way to extract Euler from matrix)
|
||||||
|
break;
|
||||||
|
case TrackType::ObjectActive:
|
||||||
|
if (track.target) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutscenePlayer::stop() {
|
||||||
|
if (!m_active) return;
|
||||||
|
m_active = nullptr;
|
||||||
|
fireOnComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CutscenePlayer::hasCameraTracks() const {
|
||||||
|
if (!m_active) return false;
|
||||||
|
for (uint8_t i = 0; i < m_active->trackCount; i++) {
|
||||||
|
auto t = m_active->tracks[i].trackType;
|
||||||
|
if (t == TrackType::CameraPosition || t == TrackType::CameraRotation)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutscenePlayer::tick() {
|
||||||
|
if (!m_active) return;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < m_active->trackCount; i++) {
|
||||||
|
applyTrack(m_active->tracks[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (m_nextAudio < m_active->audioEventCount) {
|
||||||
|
CutsceneAudioEvent& evt = m_active->audioEvents[m_nextAudio];
|
||||||
|
if (evt.frame <= m_frame) {
|
||||||
|
if (m_audio) {
|
||||||
|
m_audio->play(evt.clipIndex, evt.volume, evt.pan);
|
||||||
|
}
|
||||||
|
m_nextAudio++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_frame++;
|
||||||
|
if (m_frame > m_active->totalFrames) {
|
||||||
|
if (m_loop) {
|
||||||
|
// Restart from the beginning
|
||||||
|
m_frame = 0;
|
||||||
|
m_nextAudio = 0;
|
||||||
|
} else {
|
||||||
|
m_active = nullptr; // Cutscene finished
|
||||||
|
fireOnComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutscenePlayer::fireOnComplete() {
|
||||||
|
if (m_onCompleteRef == LUA_NOREF || !m_luaState) return;
|
||||||
|
psyqo::Lua L(m_luaState);
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, m_onCompleteRef);
|
||||||
|
if (L.isFunction(-1)) {
|
||||||
|
if (L.pcall(0, 0) != LUA_OK) {
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
// Unreference the callback (one-shot)
|
||||||
|
luaL_unref(m_luaState, LUA_REGISTRYINDEX, m_onCompleteRef);
|
||||||
|
m_onCompleteRef = LUA_NOREF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutscenePlayer::applyTrack(CutsceneTrack& track) {
|
||||||
|
if (track.keyframeCount == 0 || !track.keyframes) return;
|
||||||
|
|
||||||
|
int16_t out[3];
|
||||||
|
|
||||||
|
switch (track.trackType) {
|
||||||
|
case TrackType::CameraPosition: {
|
||||||
|
if (!m_camera) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out);
|
||||||
|
psyqo::FixedPoint<12> x, y, z;
|
||||||
|
x.value = (int32_t)out[0];
|
||||||
|
y.value = (int32_t)out[1];
|
||||||
|
z.value = (int32_t)out[2];
|
||||||
|
m_camera->SetPosition(x, y, z);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::CameraRotation: {
|
||||||
|
if (!m_camera) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out);
|
||||||
|
psyqo::Angle rx, ry, rz;
|
||||||
|
rx.value = (int32_t)out[0];
|
||||||
|
ry.value = (int32_t)out[1];
|
||||||
|
rz.value = (int32_t)out[2];
|
||||||
|
m_camera->SetRotation(rx, ry, rz);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::ObjectPosition: {
|
||||||
|
if (!track.target) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out);
|
||||||
|
track.target->position.x.value = (int32_t)out[0];
|
||||||
|
track.target->position.y.value = (int32_t)out[1];
|
||||||
|
track.target->position.z.value = (int32_t)out[2];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::ObjectRotation: {
|
||||||
|
if (!track.target) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out);
|
||||||
|
psyqo::Angle rx, ry, rz;
|
||||||
|
rx.value = (int32_t)out[0];
|
||||||
|
ry.value = (int32_t)out[1];
|
||||||
|
rz.value = (int32_t)out[2];
|
||||||
|
auto matY = psyqo::SoftMath::generateRotationMatrix33(ry, psyqo::SoftMath::Axis::Y, m_trig);
|
||||||
|
auto matX = psyqo::SoftMath::generateRotationMatrix33(rx, psyqo::SoftMath::Axis::X, m_trig);
|
||||||
|
auto matZ = psyqo::SoftMath::generateRotationMatrix33(rz, psyqo::SoftMath::Axis::Z, m_trig);
|
||||||
|
auto temp = psyqo::SoftMath::multiplyMatrix33(matY, matX);
|
||||||
|
track.target->rotation = psyqo::SoftMath::multiplyMatrix33(temp, matZ);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::ObjectActive: {
|
||||||
|
if (!track.target) return;
|
||||||
|
CutsceneKeyframe* kf = track.keyframes;
|
||||||
|
uint8_t count = track.keyframeCount;
|
||||||
|
int16_t activeVal = (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) {
|
||||||
|
activeVal = kf[i].values[0];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, 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;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, track.initialValues, out);
|
||||||
|
m_uiSystem->setPosition(track.uiHandle, out[0], out[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case TrackType::UIColor: {
|
||||||
|
if (!m_uiSystem) return;
|
||||||
|
psxsplash::lerpKeyframes(track.keyframes, track.keyframeCount, m_frame, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
135
src/cutscene.hh
Normal file
135
src/cutscene.hh
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/trigonometry.hh>
|
||||||
|
#include <psyqo/soft-math.hh>
|
||||||
|
|
||||||
|
#include "camera.hh"
|
||||||
|
#include "gameobject.hh"
|
||||||
|
#include "audiomanager.hh"
|
||||||
|
|
||||||
|
#include <psyqo-lua/lua.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
class UISystem; // Forward declaration
|
||||||
|
|
||||||
|
static constexpr int MAX_CUTSCENES = 16;
|
||||||
|
static constexpr int MAX_TRACKS = 8;
|
||||||
|
static constexpr int MAX_KEYFRAMES = 64;
|
||||||
|
static constexpr int MAX_AUDIO_EVENTS = 64;
|
||||||
|
|
||||||
|
enum class TrackType : uint8_t {
|
||||||
|
CameraPosition = 0,
|
||||||
|
CameraRotation = 1,
|
||||||
|
ObjectPosition = 2,
|
||||||
|
ObjectRotation = 3,
|
||||||
|
ObjectActive = 4,
|
||||||
|
UICanvasVisible = 5,
|
||||||
|
UIElementVisible= 6,
|
||||||
|
UIProgress = 7,
|
||||||
|
UIPosition = 8,
|
||||||
|
UIColor = 9,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class InterpMode : uint8_t {
|
||||||
|
Linear = 0,
|
||||||
|
Step = 1,
|
||||||
|
EaseIn = 2,
|
||||||
|
EaseOut = 3,
|
||||||
|
EaseInOut = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CutsceneKeyframe {
|
||||||
|
// Upper 3 bits = InterpMode (0-7), lower 13 bits = frame number (0-8191).
|
||||||
|
// At 30fps, max frame 8191 ≈ 4.5 minutes.
|
||||||
|
uint16_t frameAndInterp;
|
||||||
|
int16_t values[3];
|
||||||
|
|
||||||
|
uint16_t getFrame() const { return frameAndInterp & 0x1FFF; }
|
||||||
|
InterpMode getInterp() const { return static_cast<InterpMode>(frameAndInterp >> 13); }
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CutsceneKeyframe) == 8, "CutsceneKeyframe must be 8 bytes");
|
||||||
|
|
||||||
|
struct CutsceneAudioEvent {
|
||||||
|
uint16_t frame;
|
||||||
|
uint8_t clipIndex;
|
||||||
|
uint8_t volume;
|
||||||
|
uint8_t pan;
|
||||||
|
uint8_t pad[3];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CutsceneAudioEvent) == 8, "CutsceneAudioEvent must be 8 bytes");
|
||||||
|
|
||||||
|
struct CutsceneTrack {
|
||||||
|
TrackType trackType;
|
||||||
|
uint8_t keyframeCount;
|
||||||
|
uint8_t pad[2];
|
||||||
|
CutsceneKeyframe* keyframes;
|
||||||
|
GameObject* target;
|
||||||
|
int16_t uiHandle;
|
||||||
|
int16_t initialValues[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cutscene {
|
||||||
|
const char* name; // Points into splashpack data
|
||||||
|
uint16_t totalFrames;
|
||||||
|
uint8_t trackCount;
|
||||||
|
uint8_t audioEventCount;
|
||||||
|
CutsceneTrack tracks[MAX_TRACKS];
|
||||||
|
CutsceneAudioEvent* audioEvents; // Points into splashpack data
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
UISystem* uiSystem = nullptr);
|
||||||
|
|
||||||
|
/// Play cutscene by name. Returns false if not found.
|
||||||
|
/// If loop is true, the cutscene replays from the start when it ends.
|
||||||
|
bool play(const char* name, bool loop = false);
|
||||||
|
|
||||||
|
/// Stop the current cutscene immediately.
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
/// True if a cutscene is currently active.
|
||||||
|
bool isPlaying() const { return m_active != nullptr; }
|
||||||
|
|
||||||
|
/// True if the active cutscene has camera tracks (position or rotation).
|
||||||
|
/// Use this to decide whether to suppress player camera follow.
|
||||||
|
bool hasCameraTracks() const;
|
||||||
|
|
||||||
|
/// Set a Lua registry reference to call when the cutscene finishes.
|
||||||
|
/// Pass LUA_NOREF to clear. The callback is called ONCE when the
|
||||||
|
/// cutscene ends (not on each loop iteration - only when it truly stops).
|
||||||
|
void setOnCompleteRef(int ref) { m_onCompleteRef = ref; }
|
||||||
|
int getOnCompleteRef() const { return m_onCompleteRef; }
|
||||||
|
|
||||||
|
/// Set the lua_State for callbacks. Must be called before play().
|
||||||
|
void setLuaState(lua_State* L) { m_luaState = L; }
|
||||||
|
|
||||||
|
/// Advance one frame. Call once per frame. Does nothing when idle.
|
||||||
|
void tick();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Cutscene* m_cutscenes = nullptr;
|
||||||
|
int m_count = 0;
|
||||||
|
Cutscene* m_active = nullptr;
|
||||||
|
uint16_t m_frame = 0;
|
||||||
|
uint8_t m_nextAudio = 0;
|
||||||
|
bool m_loop = false;
|
||||||
|
Camera* m_camera = nullptr;
|
||||||
|
AudioManager* m_audio = nullptr;
|
||||||
|
UISystem* m_uiSystem = nullptr;
|
||||||
|
lua_State* m_luaState = nullptr;
|
||||||
|
int m_onCompleteRef = LUA_NOREF;
|
||||||
|
|
||||||
|
psyqo::Trig<> m_trig;
|
||||||
|
|
||||||
|
void applyTrack(CutsceneTrack& track);
|
||||||
|
void fireOnComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
46
src/fileloader.cpp
Normal file
46
src/fileloader.cpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#include "fileloader.hh"
|
||||||
|
#include <psyqo/xprintf.h>
|
||||||
|
|
||||||
|
// ── Backend selection ────────────────────────────────────────────
|
||||||
|
// LOADER_CDROM is defined by the Makefile when LOADER=cdrom.
|
||||||
|
// Default (including PCDRV_SUPPORT=1) selects the PCdrv backend.
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
#include "fileloader_cdrom.hh"
|
||||||
|
#else
|
||||||
|
#include "fileloader_pcdrv.hh"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// ── Singleton ────────────────────────────────────────────────────
|
||||||
|
FileLoader& FileLoader::Get() {
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
static FileLoaderCDRom instance;
|
||||||
|
#else
|
||||||
|
static FileLoaderPCdrv instance;
|
||||||
|
#endif
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filename helpers ─────────────────────────────────────────────
|
||||||
|
// PCdrv uses lowercase names matching the files SplashControlPanel
|
||||||
|
// writes to PSXBuild/. CDRom uses uppercase 8.3 ISO9660 names with
|
||||||
|
// the mandatory ";1" version suffix.
|
||||||
|
|
||||||
|
void FileLoader::BuildSceneFilename(int sceneIndex, char* out, int maxLen) {
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
snprintf(out, maxLen, "SCENE_%d.SPK;1", sceneIndex);
|
||||||
|
#else
|
||||||
|
snprintf(out, maxLen, "scene_%d.splashpack", sceneIndex);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileLoader::BuildLoadingFilename(int sceneIndex, char* out, int maxLen) {
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
snprintf(out, maxLen, "SCENE_%d.LDG;1", sceneIndex);
|
||||||
|
#else
|
||||||
|
snprintf(out, maxLen, "scene_%d.loading", sceneIndex);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
116
src/fileloader.hh
Normal file
116
src/fileloader.hh
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/task.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileLoader — abstract interface for loading files on PS1.
|
||||||
|
*
|
||||||
|
* Two compile-time backends:
|
||||||
|
* - FileLoaderPCdrv: PCdrv protocol (emulator break instructions OR SIO1 serial)
|
||||||
|
* - FileLoaderCDRom: CD-ROM via psyqo CDRomDevice + ISO9660Parser
|
||||||
|
*
|
||||||
|
* Build with LOADER=pcdrv (default) or LOADER=cdrom to select the backend.
|
||||||
|
*
|
||||||
|
* Both backends expose the same task-based API following the psyqo TaskQueue
|
||||||
|
* pattern (see nugget/psyqo/examples/task-demo). For PCdrv the tasks resolve
|
||||||
|
* synchronously; for CD-ROM they chain real async hardware I/O.
|
||||||
|
*
|
||||||
|
* The active backend singleton is accessed through FileLoader::Get().
|
||||||
|
*/
|
||||||
|
class FileLoader {
|
||||||
|
public:
|
||||||
|
virtual ~FileLoader() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called once from Application::prepare() before any GPU work.
|
||||||
|
* CDRom backend uses this to call CDRomDevice::prepare().
|
||||||
|
* PCdrv backend is a no-op.
|
||||||
|
*/
|
||||||
|
virtual void prepare() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Task that initialises the loader.
|
||||||
|
*
|
||||||
|
* PCdrv: calls pcdrv_sio1_init() + pcdrv_init(), resolves immediately.
|
||||||
|
* CDRom: chains CDRomDevice::scheduleReset + ISO9660Parser::scheduleInitialize.
|
||||||
|
*/
|
||||||
|
virtual psyqo::TaskQueue::Task scheduleInit() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Task that loads a file.
|
||||||
|
*
|
||||||
|
* On resolve, *outBuffer points to the loaded data (caller owns it)
|
||||||
|
* and *outSize contains the size in bytes.
|
||||||
|
* On reject, *outBuffer == nullptr and *outSize == 0.
|
||||||
|
*
|
||||||
|
* PCdrv filenames: relative paths like "scene_0.splashpack".
|
||||||
|
* CDRom filenames: ISO9660 names like "SCENE_0.SPK;1".
|
||||||
|
*
|
||||||
|
* Use BuildSceneFilename / BuildLoadingFilename helpers to get the
|
||||||
|
* correct filename for the active backend.
|
||||||
|
*/
|
||||||
|
virtual psyqo::TaskQueue::Task scheduleLoadFile(
|
||||||
|
const char* filename, uint8_t*& outBuffer, int& outSize) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronously loads a file. Provided for call sites that cannot
|
||||||
|
* easily be converted to task chains (e.g. SceneManager scene transitions).
|
||||||
|
*
|
||||||
|
* CDRom backend: uses blocking readSectorsBlocking via GPU spin-loop.
|
||||||
|
* PCdrv backend: same as the sync pcdrv_open/read/close flow.
|
||||||
|
*
|
||||||
|
* Returns loaded data (caller-owned), or nullptr on failure.
|
||||||
|
*/
|
||||||
|
virtual uint8_t* LoadFileSync(const char* filename, int& outSize) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional progress-reporting variant of LoadFileSync.
|
||||||
|
*
|
||||||
|
* @param progress If non-null, the backend may call progress->fn()
|
||||||
|
* periodically during the load with interpolated
|
||||||
|
* percentage values between startPercent and endPercent.
|
||||||
|
*
|
||||||
|
* Default implementation delegates to LoadFileSync and calls the
|
||||||
|
* callback once at endPercent. CDRom backend overrides this to
|
||||||
|
* read in 64 KB chunks and report after each chunk.
|
||||||
|
*/
|
||||||
|
struct LoadProgressInfo {
|
||||||
|
void (*fn)(uint8_t percent, void* userData);
|
||||||
|
void* userData;
|
||||||
|
uint8_t startPercent;
|
||||||
|
uint8_t endPercent;
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual uint8_t* LoadFileSyncWithProgress(
|
||||||
|
const char* filename, int& outSize,
|
||||||
|
const LoadProgressInfo* progress)
|
||||||
|
{
|
||||||
|
auto* data = LoadFileSync(filename, outSize);
|
||||||
|
if (progress && progress->fn)
|
||||||
|
progress->fn(progress->endPercent, progress->userData);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Free a buffer returned by scheduleLoadFile or LoadFileSync. */
|
||||||
|
virtual void FreeFile(uint8_t* data) = 0;
|
||||||
|
|
||||||
|
/** Human-readable name for logging ("pcdrv" / "cdrom"). */
|
||||||
|
virtual const char* Name() const = 0;
|
||||||
|
|
||||||
|
// ── Filename helpers ──────────────────────────────────────────
|
||||||
|
// Build the correct filename for the active backend.
|
||||||
|
|
||||||
|
/** scene_N.splashpack or SCENE_N.SPK;1 */
|
||||||
|
static void BuildSceneFilename(int sceneIndex, char* out, int maxLen);
|
||||||
|
|
||||||
|
/** scene_N.loading or SCENE_N.LDG;1 */
|
||||||
|
static void BuildLoadingFilename(int sceneIndex, char* out, int maxLen);
|
||||||
|
|
||||||
|
// ── Singleton ─────────────────────────────────────────────────
|
||||||
|
static FileLoader& Get();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
233
src/fileloader_cdrom.hh
Normal file
233
src/fileloader_cdrom.hh
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fileloader.hh"
|
||||||
|
|
||||||
|
#include <psyqo/cdrom-device.hh>
|
||||||
|
#include <psyqo/iso9660-parser.hh>
|
||||||
|
#include <psyqo/gpu.hh>
|
||||||
|
#include <psyqo/task.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileLoaderCDRom — loads files from CD-ROM using the psyqo ISO9660 parser.
|
||||||
|
*
|
||||||
|
* Follows the same pattern as nugget/psyqo/examples/task-demo:
|
||||||
|
* 1. CDRomDevice::prepare() — called from Application::prepare()
|
||||||
|
* 2. CDRomDevice::scheduleReset() — reset the drive
|
||||||
|
* 3. ISO9660Parser::scheduleInitialize() — parse the PVD and root dir
|
||||||
|
* 4. ISO9660Parser::scheduleGetDirentry — look up a file by path
|
||||||
|
* 5. ISO9660Parser::scheduleReadRequest — read the file sectors
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
class FileLoaderCDRom final : public FileLoader {
|
||||||
|
public:
|
||||||
|
FileLoaderCDRom() : m_isoParser(&m_cdrom) {}
|
||||||
|
|
||||||
|
// ── prepare: must be called from Application::prepare() ──────
|
||||||
|
void prepare() override {
|
||||||
|
m_cdrom.prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── scheduleInit ─────────────────────────────────────────────
|
||||||
|
// Chains: reset CD drive → initialise ISO9660 parser.
|
||||||
|
psyqo::TaskQueue::Task scheduleInit() override {
|
||||||
|
m_initQueue
|
||||||
|
.startWith(m_cdrom.scheduleReset())
|
||||||
|
.then(m_isoParser.scheduleInitialize());
|
||||||
|
return m_initQueue.schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── scheduleLoadFile ─────────────────────────────────────────
|
||||||
|
// Chains: getDirentry → allocate + read.
|
||||||
|
// The lambda captures filename/outBuffer/outSize by reference;
|
||||||
|
// they must remain valid until the owning TaskQueue completes.
|
||||||
|
psyqo::TaskQueue::Task scheduleLoadFile(
|
||||||
|
const char* filename, uint8_t*& outBuffer, int& outSize) override
|
||||||
|
{
|
||||||
|
return psyqo::TaskQueue::Task(
|
||||||
|
[this, filename, &outBuffer, &outSize](psyqo::TaskQueue::Task* task) {
|
||||||
|
// Stash the parent task so callbacks can resolve/reject it.
|
||||||
|
m_pendingTask = task;
|
||||||
|
m_pOutBuffer = &outBuffer;
|
||||||
|
m_pOutSize = &outSize;
|
||||||
|
outBuffer = nullptr;
|
||||||
|
outSize = 0;
|
||||||
|
|
||||||
|
// Step 1 — look up the directory entry.
|
||||||
|
m_isoParser.getDirentry(
|
||||||
|
filename, &m_request.entry,
|
||||||
|
[this](bool success) {
|
||||||
|
if (!success ||
|
||||||
|
m_request.entry.type ==
|
||||||
|
psyqo::ISO9660Parser::DirEntry::INVALID) {
|
||||||
|
m_pendingTask->reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2 — allocate a sector-aligned buffer and read.
|
||||||
|
uint32_t sectors =
|
||||||
|
(m_request.entry.size + 2047) / 2048;
|
||||||
|
uint8_t* buf = new uint8_t[sectors * 2048];
|
||||||
|
*m_pOutBuffer = buf;
|
||||||
|
*m_pOutSize = static_cast<int>(m_request.entry.size);
|
||||||
|
m_request.buffer = buf;
|
||||||
|
|
||||||
|
// Step 3 — chain the actual CD read via a sub-queue.
|
||||||
|
m_readQueue
|
||||||
|
.startWith(
|
||||||
|
m_isoParser.scheduleReadRequest(&m_request))
|
||||||
|
.then([this](psyqo::TaskQueue::Task* inner) {
|
||||||
|
// Read complete — resolve the outer task.
|
||||||
|
m_pendingTask->resolve();
|
||||||
|
inner->resolve();
|
||||||
|
})
|
||||||
|
.butCatch([this](psyqo::TaskQueue*) {
|
||||||
|
// Read failed — clean up and reject.
|
||||||
|
delete[] *m_pOutBuffer;
|
||||||
|
*m_pOutBuffer = nullptr;
|
||||||
|
*m_pOutSize = 0;
|
||||||
|
m_pendingTask->reject();
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LoadFileSyncWithProgress ───────────────────────────────
|
||||||
|
// Reads the file in 32-sector (64 KB) chunks, calling the
|
||||||
|
// progress callback between each chunk so the loading bar
|
||||||
|
// animates during the CD-ROM transfer.
|
||||||
|
uint8_t* LoadFileSyncWithProgress(
|
||||||
|
const char* filename, int& outSize,
|
||||||
|
const LoadProgressInfo* progress) override
|
||||||
|
{
|
||||||
|
outSize = 0;
|
||||||
|
if (!m_isoParser.initialized()) return nullptr;
|
||||||
|
|
||||||
|
// --- getDirentry (blocking via one-shot TaskQueue) ---
|
||||||
|
psyqo::ISO9660Parser::DirEntry entry;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
m_syncQueue
|
||||||
|
.startWith(m_isoParser.scheduleGetDirentry(filename, &entry))
|
||||||
|
.then([&found](psyqo::TaskQueue::Task* t) {
|
||||||
|
found = true;
|
||||||
|
t->resolve();
|
||||||
|
})
|
||||||
|
.butCatch([](psyqo::TaskQueue*) {})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
while (m_syncQueue.isRunning()) {
|
||||||
|
m_gpu->pumpCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found || entry.type == psyqo::ISO9660Parser::DirEntry::INVALID)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// --- chunked sector read with progress ---
|
||||||
|
uint32_t totalSectors = (entry.size + 2047) / 2048;
|
||||||
|
uint8_t* buf = new uint8_t[totalSectors * 2048];
|
||||||
|
|
||||||
|
static constexpr uint32_t kChunkSectors = 32; // 64 KB per chunk
|
||||||
|
uint32_t sectorsRead = 0;
|
||||||
|
|
||||||
|
while (sectorsRead < totalSectors) {
|
||||||
|
uint32_t toRead = totalSectors - sectorsRead;
|
||||||
|
if (toRead > kChunkSectors) toRead = kChunkSectors;
|
||||||
|
|
||||||
|
bool ok = m_cdrom.readSectorsBlocking(
|
||||||
|
entry.LBA + sectorsRead, toRead,
|
||||||
|
buf + sectorsRead * 2048, *m_gpu);
|
||||||
|
if (!ok) {
|
||||||
|
delete[] buf;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
sectorsRead += toRead;
|
||||||
|
|
||||||
|
if (progress && progress->fn) {
|
||||||
|
uint8_t pct = progress->startPercent +
|
||||||
|
(uint8_t)((uint32_t)(progress->endPercent - progress->startPercent)
|
||||||
|
* sectorsRead / totalSectors);
|
||||||
|
progress->fn(pct, progress->userData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outSize = static_cast<int>(entry.size);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LoadFileSync ─────────────────────────────────────────────
|
||||||
|
// Blocking fallback for code paths that can't use tasks (e.g.
|
||||||
|
// SceneManager scene transitions). Uses the blocking readSectors
|
||||||
|
// variant which spins on GPU callbacks.
|
||||||
|
uint8_t* LoadFileSync(const char* filename, int& outSize) override {
|
||||||
|
outSize = 0;
|
||||||
|
|
||||||
|
if (!m_isoParser.initialized()) return nullptr;
|
||||||
|
|
||||||
|
// --- getDirentry (blocking via one-shot TaskQueue) ---
|
||||||
|
psyqo::ISO9660Parser::DirEntry entry;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
m_syncQueue
|
||||||
|
.startWith(m_isoParser.scheduleGetDirentry(filename, &entry))
|
||||||
|
.then([&found](psyqo::TaskQueue::Task* t) {
|
||||||
|
found = true;
|
||||||
|
t->resolve();
|
||||||
|
})
|
||||||
|
.butCatch([](psyqo::TaskQueue*) {})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
// Spin until the queue finishes (GPU callbacks service the CD IRQs).
|
||||||
|
while (m_syncQueue.isRunning()) {
|
||||||
|
m_gpu->pumpCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found || entry.type == psyqo::ISO9660Parser::DirEntry::INVALID)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// --- read sectors (blocking API) ---
|
||||||
|
uint32_t sectors = (entry.size + 2047) / 2048;
|
||||||
|
uint8_t* buf = new uint8_t[sectors * 2048];
|
||||||
|
|
||||||
|
bool ok = m_cdrom.readSectorsBlocking(
|
||||||
|
entry.LBA, sectors, buf, *m_gpu);
|
||||||
|
if (!ok) {
|
||||||
|
delete[] buf;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
outSize = static_cast<int>(entry.size);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FreeFile ─────────────────────────────────────────────────
|
||||||
|
void FreeFile(uint8_t* data) override { delete[] data; }
|
||||||
|
|
||||||
|
const char* Name() const override { return "cdrom"; }
|
||||||
|
|
||||||
|
/** Stash the GPU pointer so LoadFileSync can spin on pumpCallbacks. */
|
||||||
|
void setGPU(psyqo::GPU* gpu) { m_gpu = gpu; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
psyqo::CDRomDevice m_cdrom;
|
||||||
|
psyqo::ISO9660Parser m_isoParser;
|
||||||
|
|
||||||
|
// Sub-queues (not nested in the parent queue's fixed_vector storage).
|
||||||
|
psyqo::TaskQueue m_initQueue;
|
||||||
|
psyqo::TaskQueue m_readQueue;
|
||||||
|
psyqo::TaskQueue m_syncQueue;
|
||||||
|
|
||||||
|
// State carried across the async getDirentry→read chain.
|
||||||
|
psyqo::ISO9660Parser::ReadRequest m_request;
|
||||||
|
psyqo::TaskQueue::Task* m_pendingTask = nullptr;
|
||||||
|
uint8_t** m_pOutBuffer = nullptr;
|
||||||
|
int* m_pOutSize = nullptr;
|
||||||
|
|
||||||
|
psyqo::GPU* m_gpu = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
80
src/fileloader_pcdrv.hh
Normal file
80
src/fileloader_pcdrv.hh
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fileloader.hh"
|
||||||
|
#include "pcdrv_handler.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileLoaderPCdrv — loads files via the PCdrv protocol.
|
||||||
|
*
|
||||||
|
* Works transparently in two modes (handled by pcdrv_handler.hh):
|
||||||
|
* - Emulator mode: break instructions intercepted by PCSX-Redux
|
||||||
|
* - Real hardware mode: SIO1 serial protocol (break handler installed
|
||||||
|
* by pcdrv_sio1_init, then pcdrv_init detects which path to use)
|
||||||
|
*/
|
||||||
|
class FileLoaderPCdrv final : public FileLoader {
|
||||||
|
public:
|
||||||
|
// ── prepare: no-op for PCdrv ──────────────────────────────────
|
||||||
|
void prepare() override {}
|
||||||
|
|
||||||
|
// ── scheduleInit ──────────────────────────────────────────────
|
||||||
|
psyqo::TaskQueue::Task scheduleInit() override {
|
||||||
|
return psyqo::TaskQueue::Task([this](psyqo::TaskQueue::Task* task) {
|
||||||
|
|
||||||
|
pcdrv_sio1_init();
|
||||||
|
|
||||||
|
m_available = (pcdrv_init() == 0);
|
||||||
|
task->resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── scheduleLoadFile ──────────────────────────────────────────
|
||||||
|
psyqo::TaskQueue::Task scheduleLoadFile(
|
||||||
|
const char* filename, uint8_t*& outBuffer, int& outSize) override
|
||||||
|
{
|
||||||
|
return psyqo::TaskQueue::Task(
|
||||||
|
[this, filename, &outBuffer, &outSize](psyqo::TaskQueue::Task* task) {
|
||||||
|
outBuffer = doLoad(filename, outSize);
|
||||||
|
task->complete(outBuffer != nullptr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LoadFileSync ──────────────────────────────────────────────
|
||||||
|
uint8_t* LoadFileSync(const char* filename, int& outSize) override {
|
||||||
|
return doLoad(filename, outSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FreeFile ──────────────────────────────────────────────────
|
||||||
|
void FreeFile(uint8_t* data) override { delete[] data; }
|
||||||
|
|
||||||
|
const char* Name() const override { return "pcdrv"; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_available = false;
|
||||||
|
|
||||||
|
uint8_t* doLoad(const char* filename, int& outSize) {
|
||||||
|
outSize = 0;
|
||||||
|
if (!m_available) return nullptr;
|
||||||
|
|
||||||
|
int fd = pcdrv_open(filename, 0, 0);
|
||||||
|
if (fd < 0) return nullptr;
|
||||||
|
|
||||||
|
int size = pcdrv_seek(fd, 0, 2); // SEEK_END
|
||||||
|
if (size <= 0) { pcdrv_close(fd); return nullptr; }
|
||||||
|
pcdrv_seek(fd, 0, 0); // SEEK_SET
|
||||||
|
|
||||||
|
// 4-byte aligned for safe struct casting
|
||||||
|
int aligned = (size + 3) & ~3;
|
||||||
|
uint8_t* buf = new uint8_t[aligned];
|
||||||
|
|
||||||
|
int read = pcdrv_read(fd, buf, size);
|
||||||
|
pcdrv_close(fd);
|
||||||
|
|
||||||
|
if (read != size) { delete[] buf; return nullptr; }
|
||||||
|
outSize = size;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
201
src/fixedpoint_patch.h
Normal file
201
src/fixedpoint_patch.h
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Pre-compiled PS1 Lua bytecode for psyqo FixedPoint metatable script.
|
||||||
|
// Compiled from the R"lua(...)lua" source in psyqo-lua/src/lua.cpp.
|
||||||
|
// This intercepts the source-text loadBuffer call so NOPARSER builds work.
|
||||||
|
// 2301 bytes
|
||||||
|
|
||||||
|
static const unsigned char FIXEDPOINT_PATCHED_BYTECODE[] = {
|
||||||
|
0x1b, 0x4c, 0x75, 0x61, 0x52, 0x00, 0x01, 0x04, 0x04, 0x04, 0x04, 0x01,
|
||||||
|
0x19, 0x93, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00,
|
||||||
|
0x00, 0x1f, 0x00, 0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x07, 0x25, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x80,
|
||||||
|
0x65, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x00, 0x8a, 0x40, 0x80, 0x80,
|
||||||
|
0x86, 0x80, 0x40, 0x00, 0x87, 0xc0, 0x40, 0x01, 0xc6, 0x80, 0x40, 0x00,
|
||||||
|
0xc7, 0x00, 0xc1, 0x01, 0x25, 0x41, 0x00, 0x00, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0x81, 0x00, 0x00, 0x4a, 0x81, 0x81, 0x82, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0xc1, 0x00, 0x00, 0x4a, 0x81, 0x01, 0x83, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0x01, 0x01, 0x00, 0x4a, 0x81, 0x81, 0x83, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0x41, 0x01, 0x00, 0x4a, 0x81, 0x01, 0x84, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0x81, 0x01, 0x00, 0x4a, 0x81, 0x81, 0x84, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0xc1, 0x01, 0x00, 0x4a, 0x81, 0x01, 0x85, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0x01, 0x02, 0x00, 0x4a, 0x81, 0x81, 0x85, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0x41, 0x02, 0x00, 0x4a, 0x81, 0x01, 0x86, 0x46, 0x01, 0x40, 0x00,
|
||||||
|
0xa5, 0x81, 0x02, 0x00, 0x4a, 0x81, 0x81, 0x86, 0x1f, 0x00, 0x80, 0x00,
|
||||||
|
0x0e, 0x00, 0x00, 0x00, 0x04, 0x0b, 0x00, 0x00, 0x00, 0x46, 0x69, 0x78,
|
||||||
|
0x65, 0x64, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x00, 0x04, 0x0b, 0x00, 0x00,
|
||||||
|
0x00, 0x6e, 0x65, 0x77, 0x46, 0x72, 0x6f, 0x6d, 0x52, 0x61, 0x77, 0x00,
|
||||||
|
0x04, 0x06, 0x00, 0x00, 0x00, 0x62, 0x69, 0x74, 0x33, 0x32, 0x00, 0x04,
|
||||||
|
0x07, 0x00, 0x00, 0x00, 0x6c, 0x73, 0x68, 0x69, 0x66, 0x74, 0x00, 0x04,
|
||||||
|
0x07, 0x00, 0x00, 0x00, 0x72, 0x73, 0x68, 0x69, 0x66, 0x74, 0x00, 0x04,
|
||||||
|
0x06, 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x61, 0x64, 0x64, 0x00, 0x04, 0x06,
|
||||||
|
0x00, 0x00, 0x00, 0x5f, 0x5f, 0x73, 0x75, 0x62, 0x00, 0x04, 0x06, 0x00,
|
||||||
|
0x00, 0x00, 0x5f, 0x5f, 0x75, 0x6e, 0x6d, 0x00, 0x04, 0x05, 0x00, 0x00,
|
||||||
|
0x00, 0x5f, 0x5f, 0x65, 0x71, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x5f,
|
||||||
|
0x5f, 0x6c, 0x74, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x6c,
|
||||||
|
0x65, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x72, 0x61, 0x77, 0x00, 0x04,
|
||||||
|
0x09, 0x00, 0x00, 0x00, 0x74, 0x6f, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72,
|
||||||
|
0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x6e, 0x65, 0x77, 0x00, 0x0b, 0x00,
|
||||||
|
0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00,
|
||||||
|
0x04, 0x07, 0x00, 0x00, 0x00, 0x46, 0x00, 0x40, 0x00, 0x8b, 0x40, 0x00,
|
||||||
|
0x00, 0x8a, 0x00, 0x80, 0x80, 0xc6, 0x80, 0x40, 0x00, 0x5e, 0x00, 0x80,
|
||||||
|
0x01, 0x5f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00,
|
||||||
|
0x00, 0x04, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x6d, 0x65, 0x74,
|
||||||
|
0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00,
|
||||||
|
0x5f, 0x72, 0x61, 0x77, 0x00, 0x04, 0x0b, 0x00, 0x00, 0x00, 0x46, 0x69,
|
||||||
|
0x78, 0x65, 0x64, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
|
||||||
|
0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x07, 0x00,
|
||||||
|
0x00, 0x00, 0x46, 0x00, 0x40, 0x00, 0x81, 0x40, 0x00, 0x00, 0xc0, 0x00,
|
||||||
|
0x00, 0x00, 0x01, 0x81, 0x00, 0x00, 0x96, 0x00, 0x01, 0x01, 0x5d, 0x40,
|
||||||
|
0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x06,
|
||||||
|
0x00, 0x00, 0x00, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x00, 0x04, 0x08, 0x00,
|
||||||
|
0x00, 0x00, 0x43, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x20, 0x00, 0x04, 0x19,
|
||||||
|
0x00, 0x00, 0x00, 0x20, 0x46, 0x69, 0x78, 0x65, 0x64, 0x50, 0x6f, 0x69,
|
||||||
|
0x6e, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x74,
|
||||||
|
0x79, 0x70, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x1d, 0x00,
|
||||||
|
0x00, 0x00, 0x02, 0x00, 0x07, 0x21, 0x00, 0x00, 0x00, 0x87, 0x00, 0x40,
|
||||||
|
0x00, 0xc6, 0x40, 0x40, 0x00, 0x00, 0x01, 0x80, 0x00, 0xdd, 0x80, 0x00,
|
||||||
|
0x01, 0x18, 0x80, 0xc0, 0x01, 0x17, 0x00, 0x02, 0x80, 0xc5, 0x00, 0x80,
|
||||||
|
0x00, 0x05, 0x01, 0x00, 0x01, 0x40, 0x01, 0x80, 0x00, 0x81, 0xc1, 0x00,
|
||||||
|
0x00, 0x1d, 0x81, 0x80, 0x01, 0x0d, 0x01, 0x01, 0x01, 0xde, 0x00, 0x00,
|
||||||
|
0x01, 0xdf, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x80, 0xc6, 0x40, 0x40,
|
||||||
|
0x00, 0x00, 0x01, 0x80, 0x00, 0xdd, 0x80, 0x00, 0x01, 0x18, 0x00, 0xc1,
|
||||||
|
0x01, 0x17, 0x00, 0x02, 0x80, 0xc7, 0x00, 0xc0, 0x00, 0xdb, 0x00, 0x00,
|
||||||
|
0x00, 0x17, 0x40, 0x01, 0x80, 0xc5, 0x00, 0x80, 0x00, 0x07, 0x01, 0xc0,
|
||||||
|
0x00, 0x0d, 0x01, 0x01, 0x01, 0xde, 0x00, 0x00, 0x01, 0xdf, 0x00, 0x00,
|
||||||
|
0x00, 0x17, 0x80, 0x00, 0x80, 0xc5, 0x00, 0x80, 0x01, 0x01, 0x41, 0x01,
|
||||||
|
0x00, 0xdd, 0x40, 0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x06, 0x00, 0x00,
|
||||||
|
0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x5f, 0x72, 0x61, 0x77, 0x00, 0x04,
|
||||||
|
0x05, 0x00, 0x00, 0x00, 0x74, 0x79, 0x70, 0x65, 0x00, 0x04, 0x07, 0x00,
|
||||||
|
0x00, 0x00, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x00, 0x03, 0x0c, 0x00,
|
||||||
|
0x00, 0x00, 0x04, 0x06, 0x00, 0x00, 0x00, 0x74, 0x61, 0x62, 0x6c, 0x65,
|
||||||
|
0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x61, 0x64, 0x64, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x02,
|
||||||
|
0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x2a, 0x00,
|
||||||
|
0x00, 0x00, 0x02, 0x00, 0x07, 0x21, 0x00, 0x00, 0x00, 0x87, 0x00, 0x40,
|
||||||
|
0x00, 0xc6, 0x40, 0x40, 0x00, 0x00, 0x01, 0x80, 0x00, 0xdd, 0x80, 0x00,
|
||||||
|
0x01, 0x18, 0x80, 0xc0, 0x01, 0x17, 0x00, 0x02, 0x80, 0xc5, 0x00, 0x80,
|
||||||
|
0x00, 0x05, 0x01, 0x00, 0x01, 0x40, 0x01, 0x80, 0x00, 0x81, 0xc1, 0x00,
|
||||||
|
0x00, 0x1d, 0x81, 0x80, 0x01, 0x0e, 0x01, 0x01, 0x01, 0xde, 0x00, 0x00,
|
||||||
|
0x01, 0xdf, 0x00, 0x00, 0x00, 0x17, 0x00, 0x04, 0x80, 0xc6, 0x40, 0x40,
|
||||||
|
0x00, 0x00, 0x01, 0x80, 0x00, 0xdd, 0x80, 0x00, 0x01, 0x18, 0x00, 0xc1,
|
||||||
|
0x01, 0x17, 0x00, 0x02, 0x80, 0xc7, 0x00, 0xc0, 0x00, 0xdb, 0x00, 0x00,
|
||||||
|
0x00, 0x17, 0x40, 0x01, 0x80, 0xc5, 0x00, 0x80, 0x00, 0x07, 0x01, 0xc0,
|
||||||
|
0x00, 0x0e, 0x01, 0x01, 0x01, 0xde, 0x00, 0x00, 0x01, 0xdf, 0x00, 0x00,
|
||||||
|
0x00, 0x17, 0x80, 0x00, 0x80, 0xc5, 0x00, 0x80, 0x01, 0x01, 0x41, 0x01,
|
||||||
|
0x00, 0xdd, 0x40, 0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x06, 0x00, 0x00,
|
||||||
|
0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x5f, 0x72, 0x61, 0x77, 0x00, 0x04,
|
||||||
|
0x05, 0x00, 0x00, 0x00, 0x74, 0x79, 0x70, 0x65, 0x00, 0x04, 0x07, 0x00,
|
||||||
|
0x00, 0x00, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x00, 0x03, 0x0c, 0x00,
|
||||||
|
0x00, 0x00, 0x04, 0x06, 0x00, 0x00, 0x00, 0x74, 0x61, 0x62, 0x6c, 0x65,
|
||||||
|
0x00, 0x04, 0x09, 0x00, 0x00, 0x00, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61,
|
||||||
|
0x63, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x01, 0x01, 0x02, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c,
|
||||||
|
0x00, 0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x06, 0x00,
|
||||||
|
0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x87, 0x00, 0x40, 0x00, 0x93, 0x00,
|
||||||
|
0x00, 0x01, 0x5e, 0x00, 0x00, 0x01, 0x5f, 0x00, 0x00, 0x00, 0x1f, 0x00,
|
||||||
|
0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x5f,
|
||||||
|
0x72, 0x61, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x3a, 0x00,
|
||||||
|
0x00, 0x00, 0x02, 0x00, 0x06, 0x23, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40,
|
||||||
|
0x00, 0xc0, 0x00, 0x80, 0x00, 0x9d, 0x80, 0x00, 0x01, 0x18, 0x40, 0x40,
|
||||||
|
0x01, 0x17, 0x80, 0x02, 0x80, 0x87, 0x80, 0xc0, 0x00, 0x9b, 0x00, 0x00,
|
||||||
|
0x00, 0x17, 0xc0, 0x01, 0x80, 0x87, 0x80, 0x40, 0x00, 0xc7, 0x80, 0xc0,
|
||||||
|
0x00, 0x58, 0xc0, 0x00, 0x01, 0x17, 0x00, 0x00, 0x80, 0x83, 0x40, 0x00,
|
||||||
|
0x00, 0x83, 0x00, 0x80, 0x00, 0x9f, 0x00, 0x00, 0x01, 0x17, 0x40, 0x04,
|
||||||
|
0x80, 0x86, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x00, 0x9d, 0x80, 0x00,
|
||||||
|
0x01, 0x18, 0xc0, 0x40, 0x01, 0x17, 0x80, 0x02, 0x80, 0x87, 0x80, 0x40,
|
||||||
|
0x00, 0xc5, 0x00, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x41, 0x01, 0x01,
|
||||||
|
0x00, 0xdd, 0x80, 0x80, 0x01, 0x58, 0xc0, 0x00, 0x01, 0x17, 0x00, 0x00,
|
||||||
|
0x80, 0x83, 0x40, 0x00, 0x00, 0x83, 0x00, 0x80, 0x00, 0x9f, 0x00, 0x00,
|
||||||
|
0x01, 0x17, 0x40, 0x00, 0x80, 0x83, 0x00, 0x00, 0x00, 0x9f, 0x00, 0x00,
|
||||||
|
0x01, 0x1f, 0x00, 0x80, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x05, 0x00,
|
||||||
|
0x00, 0x00, 0x74, 0x79, 0x70, 0x65, 0x00, 0x04, 0x06, 0x00, 0x00, 0x00,
|
||||||
|
0x74, 0x61, 0x62, 0x6c, 0x65, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x5f,
|
||||||
|
0x72, 0x61, 0x77, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6d,
|
||||||
|
0x62, 0x65, 0x72, 0x00, 0x03, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x3c, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x02, 0x00, 0x06,
|
||||||
|
0x24, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x00,
|
||||||
|
0x9d, 0x80, 0x00, 0x01, 0x18, 0x40, 0x40, 0x01, 0x17, 0x80, 0x02, 0x80,
|
||||||
|
0x87, 0x80, 0xc0, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x17, 0xc0, 0x01, 0x80,
|
||||||
|
0x87, 0x80, 0x40, 0x00, 0xc7, 0x80, 0xc0, 0x00, 0x59, 0xc0, 0x00, 0x01,
|
||||||
|
0x17, 0x00, 0x00, 0x80, 0x83, 0x40, 0x00, 0x00, 0x83, 0x00, 0x80, 0x00,
|
||||||
|
0x9f, 0x00, 0x00, 0x01, 0x17, 0x80, 0x04, 0x80, 0x86, 0x00, 0x40, 0x00,
|
||||||
|
0xc0, 0x00, 0x80, 0x00, 0x9d, 0x80, 0x00, 0x01, 0x18, 0xc0, 0x40, 0x01,
|
||||||
|
0x17, 0x80, 0x02, 0x80, 0x87, 0x80, 0x40, 0x00, 0xc5, 0x00, 0x80, 0x00,
|
||||||
|
0x00, 0x01, 0x80, 0x00, 0x41, 0x01, 0x01, 0x00, 0xdd, 0x80, 0x80, 0x01,
|
||||||
|
0x59, 0xc0, 0x00, 0x01, 0x17, 0x00, 0x00, 0x80, 0x83, 0x40, 0x00, 0x00,
|
||||||
|
0x83, 0x00, 0x80, 0x00, 0x9f, 0x00, 0x00, 0x01, 0x17, 0x80, 0x00, 0x80,
|
||||||
|
0x85, 0x00, 0x00, 0x01, 0xc1, 0x40, 0x01, 0x00, 0x9d, 0x40, 0x00, 0x01,
|
||||||
|
0x1f, 0x00, 0x80, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x05, 0x00, 0x00,
|
||||||
|
0x00, 0x74, 0x79, 0x70, 0x65, 0x00, 0x04, 0x06, 0x00, 0x00, 0x00, 0x74,
|
||||||
|
0x61, 0x62, 0x6c, 0x65, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x5f, 0x72,
|
||||||
|
0x61, 0x77, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x6e, 0x75, 0x6d, 0x62,
|
||||||
|
0x65, 0x72, 0x00, 0x03, 0x0c, 0x00, 0x00, 0x00, 0x04, 0x08, 0x00, 0x00,
|
||||||
|
0x00, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x01, 0x04, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x02,
|
||||||
|
0x00, 0x06, 0x24, 0x00, 0x00, 0x00, 0x86, 0x00, 0x40, 0x00, 0xc0, 0x00,
|
||||||
|
0x80, 0x00, 0x9d, 0x80, 0x00, 0x01, 0x18, 0x40, 0x40, 0x01, 0x17, 0x80,
|
||||||
|
0x02, 0x80, 0x87, 0x80, 0xc0, 0x00, 0x9b, 0x00, 0x00, 0x00, 0x17, 0xc0,
|
||||||
|
0x01, 0x80, 0x87, 0x80, 0x40, 0x00, 0xc7, 0x80, 0xc0, 0x00, 0x5a, 0xc0,
|
||||||
|
0x00, 0x01, 0x17, 0x00, 0x00, 0x80, 0x83, 0x40, 0x00, 0x00, 0x83, 0x00,
|
||||||
|
0x80, 0x00, 0x9f, 0x00, 0x00, 0x01, 0x17, 0x80, 0x04, 0x80, 0x86, 0x00,
|
||||||
|
0x40, 0x00, 0xc0, 0x00, 0x80, 0x00, 0x9d, 0x80, 0x00, 0x01, 0x18, 0xc0,
|
||||||
|
0x40, 0x01, 0x17, 0x80, 0x02, 0x80, 0x87, 0x80, 0x40, 0x00, 0xc5, 0x00,
|
||||||
|
0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x41, 0x01, 0x01, 0x00, 0xdd, 0x80,
|
||||||
|
0x80, 0x01, 0x5a, 0xc0, 0x00, 0x01, 0x17, 0x00, 0x00, 0x80, 0x83, 0x40,
|
||||||
|
0x00, 0x00, 0x83, 0x00, 0x80, 0x00, 0x9f, 0x00, 0x00, 0x01, 0x17, 0x80,
|
||||||
|
0x00, 0x80, 0x85, 0x00, 0x00, 0x01, 0xc1, 0x40, 0x01, 0x00, 0x9d, 0x40,
|
||||||
|
0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x05,
|
||||||
|
0x00, 0x00, 0x00, 0x74, 0x79, 0x70, 0x65, 0x00, 0x04, 0x06, 0x00, 0x00,
|
||||||
|
0x00, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00,
|
||||||
|
0x5f, 0x72, 0x61, 0x77, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x6e, 0x75,
|
||||||
|
0x6d, 0x62, 0x65, 0x72, 0x00, 0x03, 0x0c, 0x00, 0x00, 0x00, 0x04, 0x08,
|
||||||
|
0x00, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x65, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x01,
|
||||||
|
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x47, 0x00, 0x40, 0x00,
|
||||||
|
0x5f, 0x00, 0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||||
|
0x04, 0x05, 0x00, 0x00, 0x00, 0x5f, 0x72, 0x61, 0x77, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x00,
|
||||||
|
0x00, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x07, 0x00, 0x00,
|
||||||
|
0x00, 0x45, 0x00, 0x00, 0x00, 0x87, 0x00, 0x40, 0x00, 0x8d, 0x40, 0x40,
|
||||||
|
0x01, 0xc1, 0x80, 0x00, 0x00, 0x5e, 0x00, 0x80, 0x01, 0x5f, 0x00, 0x00,
|
||||||
|
0x00, 0x1f, 0x00, 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x05, 0x00,
|
||||||
|
0x00, 0x00, 0x5f, 0x72, 0x61, 0x77, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00,
|
||||||
|
0x03, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5d, 0x00, 0x00, 0x00, 0x60,
|
||||||
|
0x00, 0x00, 0x00, 0x02, 0x00, 0x07, 0x0f, 0x00, 0x00, 0x00, 0x18, 0x00,
|
||||||
|
0xc0, 0x00, 0x17, 0x00, 0x00, 0x80, 0x41, 0x40, 0x00, 0x00, 0x86, 0x80,
|
||||||
|
0x40, 0x00, 0xcb, 0x40, 0x00, 0x00, 0x05, 0x01, 0x80, 0x00, 0x40, 0x01,
|
||||||
|
0x00, 0x00, 0x81, 0x01, 0x01, 0x00, 0x1d, 0x81, 0x80, 0x01, 0x0d, 0x41,
|
||||||
|
0x00, 0x02, 0xca, 0x00, 0x81, 0x81, 0x06, 0x41, 0x41, 0x00, 0x9e, 0x00,
|
||||||
|
0x80, 0x01, 0x9f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x80, 0x00, 0x06, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0d, 0x00, 0x00,
|
||||||
|
0x00, 0x73, 0x65, 0x74, 0x6d, 0x65, 0x74, 0x61, 0x74, 0x61, 0x62, 0x6c,
|
||||||
|
0x65, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x5f, 0x72, 0x61, 0x77, 0x00,
|
||||||
|
0x03, 0x0c, 0x00, 0x00, 0x00, 0x04, 0x0b, 0x00, 0x00, 0x00, 0x46, 0x69,
|
||||||
|
0x78, 0x65, 0x64, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <common/util/bitfield.hh>
|
||||||
#include <psyqo/matrix.hh>
|
#include <psyqo/matrix.hh>
|
||||||
#include <psyqo/vector.hh>
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
@@ -8,17 +8,68 @@
|
|||||||
|
|
||||||
namespace psxsplash {
|
namespace psxsplash {
|
||||||
|
|
||||||
|
class Lua; // Forward declaration
|
||||||
|
|
||||||
|
// Component index constants - 0xFFFF means no component
|
||||||
|
constexpr uint16_t NO_COMPONENT = 0xFFFF;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GameObject bitfield flags
|
||||||
|
*
|
||||||
|
* Bit 0: isActive - whether object is active in scene
|
||||||
|
* Bit 1: pendingEnable - flag for deferred enable (to batch Lua calls)
|
||||||
|
* Bit 2: pendingDisable - flag for deferred disable
|
||||||
|
*/
|
||||||
class GameObject final {
|
class GameObject final {
|
||||||
|
typedef Utilities::BitSpan<bool> IsActive;
|
||||||
|
typedef Utilities::BitSpan<bool, 1> PendingEnable;
|
||||||
|
typedef Utilities::BitSpan<bool, 2> PendingDisable;
|
||||||
|
typedef Utilities::BitField<IsActive, PendingEnable, PendingDisable> GameObjectFlags;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
psyqo::Vec3 position; // 12 bytes
|
union {
|
||||||
psyqo::Matrix33 rotation; // 36 bytes
|
|
||||||
uint16_t polyCount; // 2 bytes
|
|
||||||
psyqo::PrimPieces::TPageAttr texture; // 2 bytes
|
|
||||||
uint16_t clutX, clutY;
|
|
||||||
uint16_t clut[256];
|
|
||||||
union { // 4 bytes
|
|
||||||
Tri *polygons;
|
Tri *polygons;
|
||||||
uint32_t polygonsOffset;
|
uint32_t polygonsOffset;
|
||||||
};
|
};
|
||||||
|
psyqo::Vec3 position;
|
||||||
|
psyqo::Matrix33 rotation;
|
||||||
|
|
||||||
|
// Mesh data
|
||||||
|
uint16_t polyCount;
|
||||||
|
int16_t luaFileIndex;
|
||||||
|
|
||||||
|
union {
|
||||||
|
GameObjectFlags flags;
|
||||||
|
uint32_t flagsAsInt;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Component indices (0xFFFF = no component)
|
||||||
|
uint16_t interactableIndex;
|
||||||
|
uint16_t _reserved0; // Was healthIndex (legacy, kept for binary layout)
|
||||||
|
// Runtime-only: Lua event bitmask (set during RegisterGameObject)
|
||||||
|
// In the splashpack binary these 4 bytes are _reserved1 + _reserved2 (zeros).
|
||||||
|
uint32_t eventMask;
|
||||||
|
|
||||||
|
// World-space AABB (20.12 fixed-point, 24 bytes)
|
||||||
|
// Used for per-object frustum culling before iterating triangles
|
||||||
|
int32_t aabbMinX, aabbMinY, aabbMinZ;
|
||||||
|
int32_t aabbMaxX, aabbMaxY, aabbMaxZ;
|
||||||
|
|
||||||
|
// Basic accessors
|
||||||
|
bool isActive() const { return flags.get<IsActive>(); }
|
||||||
|
|
||||||
|
// setActive with Lua event support - call the version that takes Lua& for events
|
||||||
|
void setActive(bool active) { flags.set<IsActive>(active); }
|
||||||
|
|
||||||
|
// Deferred enable/disable for batched Lua calls
|
||||||
|
bool isPendingEnable() const { return flags.get<PendingEnable>(); }
|
||||||
|
bool isPendingDisable() const { return flags.get<PendingDisable>(); }
|
||||||
|
void setPendingEnable(bool pending) { flags.set<PendingEnable>(pending); }
|
||||||
|
void setPendingDisable(bool pending) { flags.set<PendingDisable>(pending); }
|
||||||
|
|
||||||
|
// Component checks
|
||||||
|
bool hasInteractable() const { return interactableIndex != NO_COMPONENT; }
|
||||||
|
};
|
||||||
|
static_assert(sizeof(GameObject) == 92, "GameObject is not 92 bytes");
|
||||||
|
|
||||||
} // namespace psxsplash
|
} // namespace psxsplash
|
||||||
141
src/gameobject_bytecode.h
Normal file
141
src/gameobject_bytecode.h
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Pre-compiled PS1 Lua bytecode for GAMEOBJECT_SCRIPT
|
||||||
|
// Generated by luac_psx (32-bit, LUA_NUMBER=long) - do not edit manually
|
||||||
|
// 943 bytes
|
||||||
|
//
|
||||||
|
// Original Lua source:
|
||||||
|
// return function(metatable)
|
||||||
|
// local get_position = metatable.get_position
|
||||||
|
// local set_position = metatable.set_position
|
||||||
|
// local get_active = metatable.get_active
|
||||||
|
// local set_active = metatable.set_active
|
||||||
|
// local get_rotation = metatable.get_rotation
|
||||||
|
// local set_rotation = metatable.set_rotation
|
||||||
|
// local get_rotationY = metatable.get_rotationY
|
||||||
|
// local set_rotationY = metatable.set_rotationY
|
||||||
|
//
|
||||||
|
// metatable.get_position = nil
|
||||||
|
// metatable.set_position = nil
|
||||||
|
// metatable.get_active = nil
|
||||||
|
// metatable.set_active = nil
|
||||||
|
// metatable.get_rotation = nil
|
||||||
|
// metatable.set_rotation = nil
|
||||||
|
// metatable.get_rotationY = nil
|
||||||
|
// metatable.set_rotationY = nil
|
||||||
|
//
|
||||||
|
// function metatable.__index(self, key)
|
||||||
|
// local raw = rawget(self, key)
|
||||||
|
// if raw ~= nil then return raw end
|
||||||
|
// if key == "position" then
|
||||||
|
// return get_position(self.__cpp_ptr)
|
||||||
|
// elseif key == "active" then
|
||||||
|
// return get_active(self.__cpp_ptr)
|
||||||
|
// elseif key == "rotation" then
|
||||||
|
// return get_rotation(self.__cpp_ptr)
|
||||||
|
// elseif key == "rotationY" then
|
||||||
|
// return get_rotationY(self.__cpp_ptr)
|
||||||
|
// end
|
||||||
|
// return nil
|
||||||
|
// end
|
||||||
|
//
|
||||||
|
// function metatable.__newindex(self, key, value)
|
||||||
|
// if key == "position" then
|
||||||
|
// set_position(self.__cpp_ptr, value)
|
||||||
|
// return
|
||||||
|
// elseif key == "active" then
|
||||||
|
// set_active(self.__cpp_ptr, value)
|
||||||
|
// return
|
||||||
|
// elseif key == "rotation" then
|
||||||
|
// set_rotation(self.__cpp_ptr, value)
|
||||||
|
// return
|
||||||
|
// elseif key == "rotationY" then
|
||||||
|
// set_rotationY(self.__cpp_ptr, value)
|
||||||
|
// return
|
||||||
|
// end
|
||||||
|
// rawset(self, key, value)
|
||||||
|
// end
|
||||||
|
// end
|
||||||
|
|
||||||
|
static const unsigned char GAMEOBJECT_BYTECODE[] = {
|
||||||
|
0x1b, 0x4c, 0x75, 0x61, 0x52, 0x00, 0x01, 0x04, 0x04, 0x04, 0x04, 0x01,
|
||||||
|
0x19, 0x93, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00,
|
||||||
|
0x00, 0x1f, 0x00, 0x00, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x0a, 0x15, 0x00, 0x00, 0x00, 0x47, 0x00, 0x40, 0x00,
|
||||||
|
0x87, 0x40, 0x40, 0x00, 0xc7, 0x80, 0x40, 0x00, 0x07, 0xc1, 0x40, 0x00,
|
||||||
|
0x47, 0x01, 0x41, 0x00, 0x87, 0x41, 0x41, 0x00, 0xc7, 0x81, 0x41, 0x00,
|
||||||
|
0x07, 0xc2, 0x41, 0x00, 0x0a, 0x00, 0x42, 0x80, 0x0a, 0x00, 0xc2, 0x80,
|
||||||
|
0x0a, 0x00, 0x42, 0x81, 0x0a, 0x00, 0xc2, 0x81, 0x0a, 0x00, 0x42, 0x82,
|
||||||
|
0x0a, 0x00, 0xc2, 0x82, 0x0a, 0x00, 0x42, 0x83, 0x0a, 0x00, 0xc2, 0x83,
|
||||||
|
0x65, 0x02, 0x00, 0x00, 0x0a, 0x40, 0x82, 0x84, 0x65, 0x42, 0x00, 0x00,
|
||||||
|
0x0a, 0x40, 0x02, 0x85, 0x1f, 0x00, 0x80, 0x00, 0x0b, 0x00, 0x00, 0x00,
|
||||||
|
0x04, 0x0d, 0x00, 0x00, 0x00, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x6f, 0x73,
|
||||||
|
0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0d, 0x00, 0x00, 0x00, 0x73,
|
||||||
|
0x65, 0x74, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00,
|
||||||
|
0x04, 0x0b, 0x00, 0x00, 0x00, 0x67, 0x65, 0x74, 0x5f, 0x61, 0x63, 0x74,
|
||||||
|
0x69, 0x76, 0x65, 0x00, 0x04, 0x0b, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74,
|
||||||
|
0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, 0x04, 0x0d, 0x00, 0x00,
|
||||||
|
0x00, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f,
|
||||||
|
0x6e, 0x00, 0x04, 0x0d, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f, 0x72,
|
||||||
|
0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0e, 0x00, 0x00,
|
||||||
|
0x00, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f,
|
||||||
|
0x6e, 0x59, 0x00, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x73, 0x65, 0x74, 0x5f,
|
||||||
|
0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x00, 0x04,
|
||||||
|
0x08, 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x00,
|
||||||
|
0x04, 0x0b, 0x00, 0x00, 0x00, 0x5f, 0x5f, 0x6e, 0x65, 0x77, 0x69, 0x6e,
|
||||||
|
0x64, 0x65, 0x78, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00,
|
||||||
|
0x21, 0x00, 0x00, 0x00, 0x02, 0x00, 0x05, 0x25, 0x00, 0x00, 0x00, 0x86,
|
||||||
|
0x00, 0x40, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x9d,
|
||||||
|
0x80, 0x80, 0x01, 0x58, 0x40, 0x40, 0x01, 0x17, 0x00, 0x00, 0x80, 0x9f,
|
||||||
|
0x00, 0x00, 0x01, 0x18, 0x80, 0xc0, 0x00, 0x17, 0x00, 0x01, 0x80, 0xc5,
|
||||||
|
0x00, 0x80, 0x00, 0x07, 0xc1, 0x40, 0x00, 0xde, 0x00, 0x00, 0x01, 0xdf,
|
||||||
|
0x00, 0x00, 0x00, 0x17, 0xc0, 0x04, 0x80, 0x18, 0x00, 0xc1, 0x00, 0x17,
|
||||||
|
0x00, 0x01, 0x80, 0xc5, 0x00, 0x00, 0x01, 0x07, 0xc1, 0x40, 0x00, 0xde,
|
||||||
|
0x00, 0x00, 0x01, 0xdf, 0x00, 0x00, 0x00, 0x17, 0x00, 0x03, 0x80, 0x18,
|
||||||
|
0x40, 0xc1, 0x00, 0x17, 0x00, 0x01, 0x80, 0xc5, 0x00, 0x80, 0x01, 0x07,
|
||||||
|
0xc1, 0x40, 0x00, 0xde, 0x00, 0x00, 0x01, 0xdf, 0x00, 0x00, 0x00, 0x17,
|
||||||
|
0x40, 0x01, 0x80, 0x18, 0x80, 0xc1, 0x00, 0x17, 0xc0, 0x00, 0x80, 0xc5,
|
||||||
|
0x00, 0x00, 0x02, 0x07, 0xc1, 0x40, 0x00, 0xde, 0x00, 0x00, 0x01, 0xdf,
|
||||||
|
0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0xdf, 0x00, 0x00, 0x01, 0x1f,
|
||||||
|
0x00, 0x80, 0x00, 0x07, 0x00, 0x00, 0x00, 0x04, 0x07, 0x00, 0x00, 0x00,
|
||||||
|
0x72, 0x61, 0x77, 0x67, 0x65, 0x74, 0x00, 0x00, 0x04, 0x09, 0x00, 0x00,
|
||||||
|
0x00, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0a,
|
||||||
|
0x00, 0x00, 0x00, 0x5f, 0x5f, 0x63, 0x70, 0x70, 0x5f, 0x70, 0x74, 0x72,
|
||||||
|
0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65,
|
||||||
|
0x00, 0x04, 0x09, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69,
|
||||||
|
0x6f, 0x6e, 0x00, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x03, 0x01, 0x05, 0x01, 0x07,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00,
|
||||||
|
0x03, 0x00, 0x07, 0x25, 0x00, 0x00, 0x00, 0x18, 0x00, 0xc0, 0x00, 0x17,
|
||||||
|
0x40, 0x01, 0x80, 0xc5, 0x00, 0x00, 0x00, 0x07, 0x41, 0x40, 0x00, 0x40,
|
||||||
|
0x01, 0x00, 0x01, 0xdd, 0x40, 0x80, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x17,
|
||||||
|
0x80, 0x05, 0x80, 0x18, 0x80, 0xc0, 0x00, 0x17, 0x40, 0x01, 0x80, 0xc5,
|
||||||
|
0x00, 0x80, 0x00, 0x07, 0x41, 0x40, 0x00, 0x40, 0x01, 0x00, 0x01, 0xdd,
|
||||||
|
0x40, 0x80, 0x01, 0x1f, 0x00, 0x80, 0x00, 0x17, 0x80, 0x03, 0x80, 0x18,
|
||||||
|
0xc0, 0xc0, 0x00, 0x17, 0x40, 0x01, 0x80, 0xc5, 0x00, 0x00, 0x01, 0x07,
|
||||||
|
0x41, 0x40, 0x00, 0x40, 0x01, 0x00, 0x01, 0xdd, 0x40, 0x80, 0x01, 0x1f,
|
||||||
|
0x00, 0x80, 0x00, 0x17, 0x80, 0x01, 0x80, 0x18, 0x00, 0xc1, 0x00, 0x17,
|
||||||
|
0x00, 0x01, 0x80, 0xc5, 0x00, 0x80, 0x01, 0x07, 0x41, 0x40, 0x00, 0x40,
|
||||||
|
0x01, 0x00, 0x01, 0xdd, 0x40, 0x80, 0x01, 0x1f, 0x00, 0x80, 0x00, 0xc6,
|
||||||
|
0x40, 0x41, 0x02, 0x00, 0x01, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x80,
|
||||||
|
0x01, 0x00, 0x01, 0xdd, 0x40, 0x00, 0x02, 0x1f, 0x00, 0x80, 0x00, 0x06,
|
||||||
|
0x00, 0x00, 0x00, 0x04, 0x09, 0x00, 0x00, 0x00, 0x70, 0x6f, 0x73, 0x69,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x5f, 0x5f,
|
||||||
|
0x63, 0x70, 0x70, 0x5f, 0x70, 0x74, 0x72, 0x00, 0x04, 0x07, 0x00, 0x00,
|
||||||
|
0x00, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x00, 0x04, 0x09, 0x00, 0x00,
|
||||||
|
0x00, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x04, 0x0a,
|
||||||
|
0x00, 0x00, 0x00, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x59,
|
||||||
|
0x00, 0x04, 0x07, 0x00, 0x00, 0x00, 0x72, 0x61, 0x77, 0x73, 0x65, 0x74,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x02, 0x01,
|
||||||
|
0x04, 0x01, 0x06, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
39
src/gtemath.cpp
Normal file
39
src/gtemath.cpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#include "gtemath.hh"
|
||||||
|
|
||||||
|
#include <psyqo/gte-kernels.hh>
|
||||||
|
#include <psyqo/gte-registers.hh>
|
||||||
|
|
||||||
|
using namespace psyqo::GTE;
|
||||||
|
|
||||||
|
void psxsplash::MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result) {
|
||||||
|
// Load matA as the rotation matrix. No prior GTE op depends on RT registers here.
|
||||||
|
writeUnsafe<PseudoRegister::Rotation>(matA);
|
||||||
|
|
||||||
|
psyqo::Vec3 t;
|
||||||
|
|
||||||
|
// Column 0 of matB: Safe write to V0 ensures rotation matrix is settled before MVMVA.
|
||||||
|
psyqo::GTE::writeSafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].x, matB.vs[1].x, matB.vs[2].x});
|
||||||
|
psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>();
|
||||||
|
// Safe read: MVMVA (8 cycles) output must be stable before reading.
|
||||||
|
t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
|
||||||
|
result->vs[0].x = t.x;
|
||||||
|
result->vs[1].x = t.y;
|
||||||
|
result->vs[2].x = t.z;
|
||||||
|
|
||||||
|
// Column 1: Unsafe V0 write is fine since MVMVA just completed (no dependency on V0 from readSafe).
|
||||||
|
psyqo::GTE::writeUnsafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].y, matB.vs[1].y, matB.vs[2].y});
|
||||||
|
// Safe nop-equivalent: the compiler inserts enough instructions between write and kernel call.
|
||||||
|
psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>();
|
||||||
|
t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
|
||||||
|
result->vs[0].y = t.x;
|
||||||
|
result->vs[1].y = t.y;
|
||||||
|
result->vs[2].y = t.z;
|
||||||
|
|
||||||
|
// Column 2: Same pattern.
|
||||||
|
psyqo::GTE::writeUnsafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].z, matB.vs[1].z, matB.vs[2].z});
|
||||||
|
psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>();
|
||||||
|
t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
|
||||||
|
result->vs[0].z = t.x;
|
||||||
|
result->vs[1].z = t.y;
|
||||||
|
result->vs[2].z = t.z;
|
||||||
|
}
|
||||||
7
src/gtemath.hh
Normal file
7
src/gtemath.hh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <psyqo/matrix.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
void MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result);
|
||||||
|
} // namespace psxsplash
|
||||||
71
src/interactable.hh
Normal file
71
src/interactable.hh
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactable component - enables player interaction with objects.
|
||||||
|
*
|
||||||
|
* When the player is within interaction radius and presses the interact button,
|
||||||
|
* the onInteract Lua event fires on the associated GameObject.
|
||||||
|
*/
|
||||||
|
struct Interactable {
|
||||||
|
// Interaction radius squared (fixed-point 12-bit, pre-squared for fast distance checks)
|
||||||
|
psyqo::FixedPoint<12> radiusSquared;
|
||||||
|
|
||||||
|
// Button index that triggers interaction (0-15, matches psyqo::AdvancedPad::Button)
|
||||||
|
uint8_t interactButton;
|
||||||
|
|
||||||
|
// Configuration flags
|
||||||
|
uint8_t flags; // bit 0: isRepeatable, bit 1: showPrompt, bit 2: requireLineOfSight, bit 3: disabled
|
||||||
|
|
||||||
|
// Cooldown between interactions (in frames)
|
||||||
|
uint16_t cooldownFrames;
|
||||||
|
|
||||||
|
// Runtime state
|
||||||
|
uint16_t currentCooldown; // Frames remaining until can interact again
|
||||||
|
uint16_t gameObjectIndex; // Index of associated GameObject
|
||||||
|
|
||||||
|
// Prompt canvas name (null-terminated, max 15 chars + null)
|
||||||
|
char promptCanvasName[16];
|
||||||
|
|
||||||
|
// Flag accessors
|
||||||
|
bool isRepeatable() const { return flags & 0x01; }
|
||||||
|
bool showPrompt() const { return flags & 0x02; }
|
||||||
|
bool requireLineOfSight() const { return flags & 0x04; }
|
||||||
|
bool isDisabled() const { return flags & 0x08; }
|
||||||
|
void setDisabled(bool disabled) {
|
||||||
|
if (disabled) flags |= 0x08;
|
||||||
|
else flags &= ~0x08;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ready to interact
|
||||||
|
bool canInteract() const {
|
||||||
|
// Non-repeatable interactions: once cooldown was set, it stays permanently
|
||||||
|
if (!isRepeatable() && cooldownFrames > 0 && currentCooldown == 0) {
|
||||||
|
// Check if we already triggered once (cooldownFrames acts as sentinel)
|
||||||
|
// We use a special value to mark "already used"
|
||||||
|
}
|
||||||
|
return currentCooldown == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when interaction happens
|
||||||
|
void triggerCooldown() {
|
||||||
|
if (isRepeatable()) {
|
||||||
|
currentCooldown = cooldownFrames;
|
||||||
|
} else {
|
||||||
|
// Non-repeatable: set to max to permanently disable
|
||||||
|
currentCooldown = 0xFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called each frame to decrement cooldown
|
||||||
|
void update() {
|
||||||
|
if (currentCooldown > 0 && currentCooldown != 0xFFFF) currentCooldown--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(Interactable) == 28, "Interactable must be 28 bytes");
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
115
src/interpolation.cpp
Normal file
115
src/interpolation.cpp
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#include "interpolation.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
int32_t applyCurve(int32_t t, InterpMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
default:
|
||||||
|
case InterpMode::Linear:
|
||||||
|
return t;
|
||||||
|
case InterpMode::Step:
|
||||||
|
return 0;
|
||||||
|
case InterpMode::EaseIn:
|
||||||
|
return (int32_t)((int64_t)t * t >> 12);
|
||||||
|
case InterpMode::EaseOut:
|
||||||
|
return (int32_t)(((int64_t)t * (8192 - t)) >> 12);
|
||||||
|
case InterpMode::EaseInOut: {
|
||||||
|
int64_t t2 = (int64_t)t * t;
|
||||||
|
int64_t t3 = t2 * t;
|
||||||
|
return (int32_t)((3 * t2 - 2 * (t3 >> 12)) >> 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool findKfPair(CutsceneKeyframe* kf, uint8_t count, uint16_t frame,
|
||||||
|
uint8_t& a, uint8_t& b, int32_t& t, int16_t out[3]) {
|
||||||
|
if (count == 0) {
|
||||||
|
out[0] = out[1] = out[2] = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (frame <= kf[0].getFrame() || count == 1) {
|
||||||
|
out[0] = kf[0].values[0];
|
||||||
|
out[1] = kf[0].values[1];
|
||||||
|
out[2] = kf[0].values[2];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (frame >= kf[count - 1].getFrame()) {
|
||||||
|
out[0] = kf[count - 1].values[0];
|
||||||
|
out[1] = kf[count - 1].values[1];
|
||||||
|
out[2] = kf[count - 1].values[2];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
b = 1;
|
||||||
|
while (b < count && kf[b].getFrame() <= frame) b++;
|
||||||
|
a = b - 1;
|
||||||
|
uint16_t span = kf[b].getFrame() - kf[a].getFrame();
|
||||||
|
if (span == 0) {
|
||||||
|
out[0] = kf[a].values[0];
|
||||||
|
out[1] = kf[a].values[1];
|
||||||
|
out[2] = kf[a].values[2];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t num = (uint32_t)(frame - kf[a].getFrame()) << 12;
|
||||||
|
int32_t rawT = (int32_t)(num / span);
|
||||||
|
t = applyCurve(rawT, kf[b].getInterp());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, uint16_t frame,
|
||||||
|
const int16_t initial[3], int16_t out[3]) {
|
||||||
|
uint8_t a, b;
|
||||||
|
int32_t t;
|
||||||
|
if (!findKfPair(kf, count, frame, a, b, t, out)) {
|
||||||
|
if (count > 0 && kf[0].getFrame() > 0 && frame < kf[0].getFrame()) {
|
||||||
|
uint16_t span = kf[0].getFrame();
|
||||||
|
uint32_t num = (uint32_t)frame << 12;
|
||||||
|
int32_t rawT = (int32_t)(num / span);
|
||||||
|
int32_t ct = applyCurve(rawT, kf[0].getInterp());
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
int32_t delta = (int32_t)kf[0].values[i] - (int32_t)initial[i];
|
||||||
|
out[i] = (int16_t)((int32_t)initial[i] + ((delta * ct) >> 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
int32_t delta = (int32_t)kf[b].values[i] - (int32_t)kf[a].values[i];
|
||||||
|
out[i] = (int16_t)((int32_t)kf[a].values[i] + ((delta * t) >> 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int32_t ANGLE_FULL_CIRCLE = 2048;
|
||||||
|
static constexpr int32_t ANGLE_HALF_CIRCLE = 1024;
|
||||||
|
|
||||||
|
void lerpAngles(CutsceneKeyframe* kf, uint8_t count, uint16_t frame,
|
||||||
|
const int16_t initial[3], int16_t out[3]) {
|
||||||
|
uint8_t a, b;
|
||||||
|
int32_t t;
|
||||||
|
if (!findKfPair(kf, count, frame, a, b, t, out)) {
|
||||||
|
if (count > 0 && kf[0].getFrame() > 0 && frame < kf[0].getFrame()) {
|
||||||
|
uint16_t span = kf[0].getFrame();
|
||||||
|
uint32_t num = (uint32_t)frame << 12;
|
||||||
|
int32_t rawT = (int32_t)(num / span);
|
||||||
|
int32_t ct = applyCurve(rawT, kf[0].getInterp());
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
int32_t from = (int32_t)initial[i];
|
||||||
|
int32_t to = (int32_t)kf[0].values[i];
|
||||||
|
int32_t delta = to - from;
|
||||||
|
delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE;
|
||||||
|
out[i] = (int16_t)(from + ((delta * ct) >> 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
int32_t from = (int32_t)kf[a].values[i];
|
||||||
|
int32_t to = (int32_t)kf[b].values[i];
|
||||||
|
int32_t delta = to - from;
|
||||||
|
delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE;
|
||||||
|
out[i] = (int16_t)(from + ((delta * t) >> 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
19
src/interpolation.hh
Normal file
19
src/interpolation.hh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "cutscene.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
int32_t applyCurve(int32_t t, InterpMode mode);
|
||||||
|
|
||||||
|
bool findKfPair(CutsceneKeyframe* kf, uint8_t count, uint16_t frame,
|
||||||
|
uint8_t& a, uint8_t& b, int32_t& t, int16_t out[3]);
|
||||||
|
|
||||||
|
void lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, uint16_t frame,
|
||||||
|
const int16_t initial[3], int16_t out[3]);
|
||||||
|
|
||||||
|
void lerpAngles(CutsceneKeyframe* kf, uint8_t count, uint16_t frame,
|
||||||
|
const int16_t initial[3], int16_t out[3]);
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
48
src/loadbuffer_patch.cpp
Normal file
48
src/loadbuffer_patch.cpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Linker --wrap intercept for luaL_loadbufferx.
|
||||||
|
//
|
||||||
|
// psyqo-lua's FixedPoint metatable setup (at the pinned nugget commit)
|
||||||
|
// loads Lua SOURCE text via loadBuffer. With NOPARSER, the parser stub
|
||||||
|
// rejects it with "parser not loaded". This wrapper intercepts that
|
||||||
|
// specific call and redirects it to pre-compiled bytecode.
|
||||||
|
//
|
||||||
|
// All other loadBuffer calls pass through to the real implementation.
|
||||||
|
|
||||||
|
#include "fixedpoint_patch.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
// The real luaL_loadbufferx provided by liblua
|
||||||
|
int __real_luaL_loadbufferx(void* L, const char* buff, unsigned int size,
|
||||||
|
const char* name, const char* mode);
|
||||||
|
|
||||||
|
int __wrap_luaL_loadbufferx(void* L, const char* buff, unsigned int size,
|
||||||
|
const char* name, const char* mode) {
|
||||||
|
// Check if this is the fixedpoint script load from psyqo-lua.
|
||||||
|
// The name is "buffer:fixedPointScript" in the pinned nugget version.
|
||||||
|
if (name) {
|
||||||
|
// Compare the first few chars to identify the fixedpoint load
|
||||||
|
const char* expected = "buffer:fixedPointScript";
|
||||||
|
const char* a = name;
|
||||||
|
const char* b = expected;
|
||||||
|
bool match = true;
|
||||||
|
while (*b) {
|
||||||
|
if (*a != *b) { match = false; break; }
|
||||||
|
a++;
|
||||||
|
b++;
|
||||||
|
}
|
||||||
|
if (match && *a == '\0') {
|
||||||
|
// Redirect to pre-compiled bytecode
|
||||||
|
return __real_luaL_loadbufferx(
|
||||||
|
L,
|
||||||
|
reinterpret_cast<const char*>(FIXEDPOINT_PATCHED_BYTECODE),
|
||||||
|
sizeof(FIXEDPOINT_PATCHED_BYTECODE),
|
||||||
|
"bytecode:fixedPointScript",
|
||||||
|
mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other calls pass through unchanged
|
||||||
|
return __real_luaL_loadbufferx(L, buff, size, name, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
383
src/loadingscreen.cpp
Normal file
383
src/loadingscreen.cpp
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
#include "loadingscreen.hh"
|
||||||
|
#include "fileloader.hh"
|
||||||
|
#include <psyqo/kernel.hh>
|
||||||
|
#include "renderer.hh"
|
||||||
|
|
||||||
|
#include <psyqo/primitives/rectangles.hh>
|
||||||
|
#include <psyqo/primitives/misc.hh>
|
||||||
|
#include <psyqo/primitives/triangles.hh>
|
||||||
|
#include <psyqo/primitives/sprites.hh>
|
||||||
|
#include <psyqo/primitives/control.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
static constexpr int16_t kBuffer1Y = 256;
|
||||||
|
|
||||||
|
// This file has duplicate code from UISystem... This is terrible...
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Load
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
bool LoadingScreen::load(psyqo::GPU& gpu, psyqo::Font<>& systemFont, int sceneIndex) {
|
||||||
|
// Build filename using the active backend's naming convention
|
||||||
|
char filename[32];
|
||||||
|
FileLoader::BuildLoadingFilename(sceneIndex, filename, sizeof(filename));
|
||||||
|
int fileSize = 0;
|
||||||
|
uint8_t* data = FileLoader::Get().LoadFileSync(filename, fileSize);
|
||||||
|
if (!data || fileSize < (int)sizeof(LoaderPackHeader)) {
|
||||||
|
if (data) FileLoader::Get().FreeFile(data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* header = reinterpret_cast<const LoaderPackHeader*>(data);
|
||||||
|
if (header->magic[0] != 'L' || header->magic[1] != 'P') {
|
||||||
|
FileLoader::Get().FreeFile(data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_data = data;
|
||||||
|
m_dataSize = fileSize;
|
||||||
|
m_font = &systemFont;
|
||||||
|
m_active = true;
|
||||||
|
m_resW = (int16_t)header->resW;
|
||||||
|
m_resH = (int16_t)header->resH;
|
||||||
|
|
||||||
|
// Upload texture atlases and CLUTs to VRAM (before setting DrawingOffset)
|
||||||
|
uploadTextures(gpu);
|
||||||
|
|
||||||
|
// Initialize UISystem with the system font and parse the loader pack
|
||||||
|
m_ui.init(systemFont);
|
||||||
|
m_ui.loadFromSplashpack(data, header->canvasCount, header->fontCount, header->tableOffset);
|
||||||
|
m_ui.uploadFonts(gpu);
|
||||||
|
|
||||||
|
// Ensure canvas 0 is visible
|
||||||
|
if (m_ui.getCanvasCount() > 0) {
|
||||||
|
m_ui.setCanvasVisible(0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the progress bar named "loading"
|
||||||
|
findProgressBar();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Upload atlas/CLUT data to VRAM (RAM → VRAM blit)
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
void LoadingScreen::uploadTextures(psyqo::GPU& gpu) {
|
||||||
|
auto* header = reinterpret_cast<const LoaderPackHeader*>(m_data);
|
||||||
|
|
||||||
|
// Atlas and CLUT entries start right after the 16-byte header
|
||||||
|
const uint8_t* ptr = m_data + sizeof(LoaderPackHeader);
|
||||||
|
|
||||||
|
for (int ai = 0; ai < header->atlasCount; ai++) {
|
||||||
|
auto* atlas = reinterpret_cast<const LoaderPackAtlas*>(ptr);
|
||||||
|
ptr += sizeof(LoaderPackAtlas);
|
||||||
|
|
||||||
|
if (atlas->pixelDataOffset != 0 && atlas->width > 0 && atlas->height > 0) {
|
||||||
|
const uint16_t* pixels = reinterpret_cast<const uint16_t*>(m_data + atlas->pixelDataOffset);
|
||||||
|
psyqo::Rect region{.a = {.x = (int16_t)atlas->x, .y = (int16_t)atlas->y},
|
||||||
|
.b = {(int16_t)atlas->width, (int16_t)atlas->height}};
|
||||||
|
gpu.uploadToVRAM(pixels, region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int ci = 0; ci < header->clutCount; ci++) {
|
||||||
|
auto* clut = reinterpret_cast<const LoaderPackClut*>(ptr);
|
||||||
|
ptr += sizeof(LoaderPackClut);
|
||||||
|
|
||||||
|
if (clut->clutDataOffset != 0 && clut->length > 0) {
|
||||||
|
const uint16_t* palette = reinterpret_cast<const uint16_t*>(m_data + clut->clutDataOffset);
|
||||||
|
psyqo::Rect region{.a = {.x = (int16_t)(clut->clutX * 16), .y = (int16_t)clut->clutY},
|
||||||
|
.b = {(int16_t)clut->length, 1}};
|
||||||
|
gpu.uploadToVRAM(palette, region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Find progress bar
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
void LoadingScreen::findProgressBar() {
|
||||||
|
m_hasProgressBar = false;
|
||||||
|
|
||||||
|
int canvasCount = m_ui.getCanvasCount();
|
||||||
|
for (int ci = 0; ci < canvasCount; ci++) {
|
||||||
|
int handle = m_ui.findElement(ci, "loading");
|
||||||
|
if (handle >= 0 && m_ui.getElementType(handle) == UIElementType::Progress) {
|
||||||
|
m_hasProgressBar = true;
|
||||||
|
|
||||||
|
m_ui.getPosition(handle, m_barX, m_barY);
|
||||||
|
m_ui.getSize(handle, m_barW, m_barH);
|
||||||
|
m_ui.getColor(handle, m_barFillR, m_barFillG, m_barFillB);
|
||||||
|
m_ui.getProgressBgColor(handle, m_barBgR, m_barBgG, m_barBgB);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Draw a filled rectangle (immediate mode, no DMA chain).
|
||||||
|
// Coordinates are logical — DrawingOffset shifts them to the target buffer.
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
void LoadingScreen::drawRect(psyqo::GPU& gpu, int16_t x, int16_t y,
|
||||||
|
int16_t w, int16_t h,
|
||||||
|
uint8_t r, uint8_t g, uint8_t b) {
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
psyqo::Prim::Rectangle rect;
|
||||||
|
rect.setColor(psyqo::Color{.r = r, .g = g, .b = b});
|
||||||
|
rect.position = {.x = x, .y = y};
|
||||||
|
rect.size = {.x = w, .y = h};
|
||||||
|
rect.setOpaque();
|
||||||
|
gpu.sendPrimitive(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Draw a textured image (two triangles, sendPrimitive).
|
||||||
|
// GouraudTexturedTriangle carries its own TPage attribute.
|
||||||
|
// DrawingOffset shifts logical coordinates to the correct framebuffer.
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
void LoadingScreen::drawImage(psyqo::GPU& gpu, int handle,
|
||||||
|
int16_t x, int16_t y, int16_t w, int16_t h,
|
||||||
|
uint8_t r, uint8_t g, uint8_t b) {
|
||||||
|
const UIImageData* img = m_ui.getImageData(handle);
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
// Build TPage attribute
|
||||||
|
psyqo::PrimPieces::TPageAttr tpage;
|
||||||
|
tpage.setPageX(img->texpageX);
|
||||||
|
tpage.setPageY(img->texpageY);
|
||||||
|
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);
|
||||||
|
|
||||||
|
psyqo::PrimPieces::ClutIndex clut(img->clutX, img->clutY);
|
||||||
|
psyqo::Color tint = {.r = r, .g = g, .b = b};
|
||||||
|
|
||||||
|
// Triangle 0: top-left, top-right, bottom-left
|
||||||
|
{
|
||||||
|
psyqo::Prim::GouraudTexturedTriangle tri;
|
||||||
|
tri.pointA.x = x; tri.pointA.y = y;
|
||||||
|
tri.pointB.x = x + w; tri.pointB.y = y;
|
||||||
|
tri.pointC.x = x; tri.pointC.y = y + h;
|
||||||
|
tri.uvA.u = img->u0; tri.uvA.v = img->v0;
|
||||||
|
tri.uvB.u = img->u1; tri.uvB.v = img->v0;
|
||||||
|
tri.uvC.u = img->u0; tri.uvC.v = img->v1;
|
||||||
|
tri.tpage = tpage;
|
||||||
|
tri.clutIndex = clut;
|
||||||
|
tri.setColorA(tint);
|
||||||
|
tri.setColorB(tint);
|
||||||
|
tri.setColorC(tint);
|
||||||
|
tri.setOpaque();
|
||||||
|
gpu.sendPrimitive(tri);
|
||||||
|
}
|
||||||
|
// Triangle 1: top-right, bottom-right, bottom-left
|
||||||
|
{
|
||||||
|
psyqo::Prim::GouraudTexturedTriangle tri;
|
||||||
|
tri.pointA.x = x + w; tri.pointA.y = y;
|
||||||
|
tri.pointB.x = x + w; tri.pointB.y = y + h;
|
||||||
|
tri.pointC.x = x; tri.pointC.y = y + h;
|
||||||
|
tri.uvA.u = img->u1; tri.uvA.v = img->v0;
|
||||||
|
tri.uvB.u = img->u1; tri.uvB.v = img->v1;
|
||||||
|
tri.uvC.u = img->u0; tri.uvC.v = img->v1;
|
||||||
|
tri.tpage = tpage;
|
||||||
|
tri.clutIndex = clut;
|
||||||
|
tri.setColorA(tint);
|
||||||
|
tri.setColorB(tint);
|
||||||
|
tri.setColorC(tint);
|
||||||
|
tri.setOpaque();
|
||||||
|
gpu.sendPrimitive(tri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Draw custom-font text via sendPrimitive (TPage + Sprite per glyph).
|
||||||
|
// DrawingOffset shifts logical coordinates to the correct framebuffer.
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
void LoadingScreen::drawCustomText(psyqo::GPU& gpu, int handle,
|
||||||
|
int16_t x, int16_t y,
|
||||||
|
uint8_t r, uint8_t g, uint8_t b) {
|
||||||
|
uint8_t fontIdx = m_ui.getTextFontIndex(handle);
|
||||||
|
if (fontIdx == 0) return; // system font, not custom
|
||||||
|
const UIFontDesc* fd = m_ui.getFontDesc(fontIdx - 1); // 1-based → 0-based
|
||||||
|
if (!fd) return;
|
||||||
|
|
||||||
|
const char* text = m_ui.getText(handle);
|
||||||
|
if (!text || !text[0]) return;
|
||||||
|
|
||||||
|
// Set TPage for this font's texture page
|
||||||
|
psyqo::Prim::TPage tpCmd;
|
||||||
|
tpCmd.attr.setPageX(fd->vramX >> 6);
|
||||||
|
tpCmd.attr.setPageY(fd->vramY >> 8);
|
||||||
|
tpCmd.attr.set(psyqo::Prim::TPageAttr::Tex4Bits);
|
||||||
|
tpCmd.attr.setDithering(false);
|
||||||
|
gpu.sendPrimitive(tpCmd);
|
||||||
|
|
||||||
|
// CLUT reference (same as renderProportionalText in UISystem)
|
||||||
|
psyqo::Vertex clutPos = {{.x = (int16_t)fd->vramX, .y = (int16_t)fd->vramY}};
|
||||||
|
psyqo::PrimPieces::ClutIndex clutIdx(clutPos);
|
||||||
|
psyqo::Color color = {.r = r, .g = g, .b = b};
|
||||||
|
|
||||||
|
int glyphsPerRow = 256 / fd->glyphW;
|
||||||
|
uint8_t baseV = fd->vramY & 0xFF;
|
||||||
|
|
||||||
|
int16_t cursorX = x;
|
||||||
|
while (*text) {
|
||||||
|
uint8_t c = (uint8_t)*text++;
|
||||||
|
if (c < 32 || c > 127) c = '?';
|
||||||
|
uint8_t charIdx = c - 32;
|
||||||
|
uint8_t advance = fd->advanceWidths[charIdx];
|
||||||
|
|
||||||
|
if (c == ' ') {
|
||||||
|
cursorX += advance;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int charRow = charIdx / glyphsPerRow;
|
||||||
|
int charCol = charIdx % glyphsPerRow;
|
||||||
|
uint8_t u = (uint8_t)(charCol * fd->glyphW);
|
||||||
|
uint8_t v = (uint8_t)(baseV + charRow * fd->glyphH);
|
||||||
|
|
||||||
|
int16_t spriteW = (advance > 0 && advance < fd->glyphW) ? (int16_t)advance : (int16_t)fd->glyphW;
|
||||||
|
|
||||||
|
psyqo::Prim::Sprite sprite;
|
||||||
|
sprite.setColor(color);
|
||||||
|
sprite.position = {.x = cursorX, .y = y};
|
||||||
|
sprite.size = {.x = spriteW, .y = (int16_t)fd->glyphH};
|
||||||
|
psyqo::PrimPieces::TexInfo texInfo;
|
||||||
|
texInfo.u = u;
|
||||||
|
texInfo.v = v;
|
||||||
|
texInfo.clut = clutIdx;
|
||||||
|
sprite.texInfo = texInfo;
|
||||||
|
gpu.sendPrimitive(sprite);
|
||||||
|
|
||||||
|
cursorX += advance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Render ALL elements to ONE framebuffer at the given VRAM Y offset.
|
||||||
|
// Uses DrawingOffset so all draw functions use logical coordinates.
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
void LoadingScreen::renderToBuffer(psyqo::GPU& gpu, int16_t yOffset) {
|
||||||
|
// Configure GPU drawing area for this framebuffer
|
||||||
|
gpu.sendPrimitive(psyqo::Prim::DrawingAreaStart(psyqo::Vertex{{.x = 0, .y = yOffset}}));
|
||||||
|
gpu.sendPrimitive(psyqo::Prim::DrawingAreaEnd(psyqo::Vertex{{.x = m_resW, .y = (int16_t)(yOffset + m_resH)}}));
|
||||||
|
gpu.sendPrimitive(psyqo::Prim::DrawingOffset(psyqo::Vertex{{.x = 0, .y = yOffset}}));
|
||||||
|
|
||||||
|
// Clear this buffer to black (Rectangle is shifted by DrawingOffset)
|
||||||
|
drawRect(gpu, 0, 0, m_resW, m_resH, 0, 0, 0);
|
||||||
|
|
||||||
|
int canvasCount = m_ui.getCanvasCount();
|
||||||
|
for (int ci = 0; ci < canvasCount; ci++) {
|
||||||
|
if (!m_ui.isCanvasVisible(ci)) continue;
|
||||||
|
int elemCount = m_ui.getCanvasElementCount(ci);
|
||||||
|
|
||||||
|
for (int ei = 0; ei < elemCount; ei++) {
|
||||||
|
int handle = m_ui.getCanvasElementHandle(ci, ei);
|
||||||
|
if (handle < 0 || !m_ui.isElementVisible(handle)) continue;
|
||||||
|
|
||||||
|
UIElementType type = m_ui.getElementType(handle);
|
||||||
|
int16_t x, y, w, h;
|
||||||
|
m_ui.getPosition(handle, x, y);
|
||||||
|
m_ui.getSize(handle, w, h);
|
||||||
|
uint8_t r, g, b;
|
||||||
|
m_ui.getColor(handle, r, g, b);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case UIElementType::Box:
|
||||||
|
drawRect(gpu, x, y, w, h, r, g, b);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIElementType::Progress: {
|
||||||
|
uint8_t bgr, bgg, bgb;
|
||||||
|
m_ui.getProgressBgColor(handle, bgr, bgg, bgb);
|
||||||
|
drawRect(gpu, x, y, w, h, bgr, bgg, bgb);
|
||||||
|
|
||||||
|
uint8_t val = m_ui.getProgress(handle);
|
||||||
|
int fillW = (int)val * w / 100;
|
||||||
|
if (fillW > 0)
|
||||||
|
drawRect(gpu, x, y, (int16_t)fillW, h, r, g, b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case UIElementType::Image:
|
||||||
|
drawImage(gpu, handle, x, y, w, h, r, g, b);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIElementType::Text: {
|
||||||
|
uint8_t fontIdx = m_ui.getTextFontIndex(handle);
|
||||||
|
if (fontIdx > 0) {
|
||||||
|
drawCustomText(gpu, handle, x, y, r, g, b);
|
||||||
|
} else if (m_font) {
|
||||||
|
m_font->print(gpu, m_ui.getText(handle),
|
||||||
|
{{.x = x, .y = y}},
|
||||||
|
{{.r = r, .g = g, .b = b}});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Render to both framebuffers, then FREE all loaded data
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
void LoadingScreen::renderInitialAndFree(psyqo::GPU& gpu) {
|
||||||
|
if (!m_data) return;
|
||||||
|
|
||||||
|
// Render to framebuffer 0 (Y = 0)
|
||||||
|
renderToBuffer(gpu, 0);
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
|
||||||
|
// Render to framebuffer 1 (Y = 256 — psyqo's hardcoded buffer-1 offset)
|
||||||
|
renderToBuffer(gpu, kBuffer1Y);
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
|
||||||
|
// Restore normal scissor for the active framebuffer
|
||||||
|
gpu.enableScissor();
|
||||||
|
|
||||||
|
// FREE all loaded data — the splashpack needs this memory
|
||||||
|
FileLoader::Get().FreeFile(m_data);
|
||||||
|
m_data = nullptr;
|
||||||
|
m_dataSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Update progress bar in BOTH framebuffers
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
void LoadingScreen::updateProgress(psyqo::GPU& gpu, uint8_t percent) {
|
||||||
|
if (!m_hasProgressBar || !m_active) return;
|
||||||
|
if (percent > 100) percent = 100;
|
||||||
|
|
||||||
|
int fillW = (int)percent * m_barW / 100;
|
||||||
|
|
||||||
|
// Draw into both framebuffers using DrawingOffset
|
||||||
|
for (int buf = 0; buf < 2; buf++) {
|
||||||
|
int16_t yOff = (buf == 0) ? 0 : kBuffer1Y;
|
||||||
|
|
||||||
|
gpu.sendPrimitive(psyqo::Prim::DrawingAreaStart(psyqo::Vertex{{.x = 0, .y = yOff}}));
|
||||||
|
gpu.sendPrimitive(psyqo::Prim::DrawingAreaEnd(psyqo::Vertex{{.x = m_resW, .y = (int16_t)(yOff + m_resH)}}));
|
||||||
|
gpu.sendPrimitive(psyqo::Prim::DrawingOffset(psyqo::Vertex{{.x = 0, .y = yOff}}));
|
||||||
|
|
||||||
|
// Background
|
||||||
|
drawRect(gpu, m_barX, m_barY, m_barW, m_barH,
|
||||||
|
m_barBgR, m_barBgG, m_barBgB);
|
||||||
|
|
||||||
|
// Fill
|
||||||
|
if (fillW > 0) {
|
||||||
|
drawRect(gpu, m_barX, m_barY, (int16_t)fillW, m_barH,
|
||||||
|
m_barFillR, m_barFillG, m_barFillB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore normal scissor
|
||||||
|
gpu.enableScissor();
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
104
src/loadingscreen.hh
Normal file
104
src/loadingscreen.hh
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/font.hh>
|
||||||
|
#include <psyqo/gpu.hh>
|
||||||
|
#include <psyqo/primitives/common.hh>
|
||||||
|
#include "uisystem.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
struct LoaderPackHeader {
|
||||||
|
char magic[2]; // "LP"
|
||||||
|
uint16_t version; // 2
|
||||||
|
uint8_t fontCount;
|
||||||
|
uint8_t canvasCount; // always 1
|
||||||
|
uint16_t resW;
|
||||||
|
uint16_t resH;
|
||||||
|
uint8_t atlasCount; // texture atlases for UI images
|
||||||
|
uint8_t clutCount; // CLUTs for indexed-color images
|
||||||
|
uint32_t tableOffset;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(LoaderPackHeader) == 16, "LoaderPackHeader must be 16 bytes");
|
||||||
|
|
||||||
|
struct LoaderPackAtlas {
|
||||||
|
uint32_t pixelDataOffset; // absolute offset in file
|
||||||
|
uint16_t width, height;
|
||||||
|
uint16_t x, y; // VRAM position
|
||||||
|
};
|
||||||
|
static_assert(sizeof(LoaderPackAtlas) == 12, "LoaderPackAtlas must be 12 bytes");
|
||||||
|
|
||||||
|
struct LoaderPackClut {
|
||||||
|
uint32_t clutDataOffset; // absolute offset in file
|
||||||
|
uint16_t clutX; // VRAM X (in 16-pixel units × 16)
|
||||||
|
uint16_t clutY; // VRAM Y
|
||||||
|
uint16_t length; // number of palette entries
|
||||||
|
uint16_t pad;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(LoaderPackClut) == 12, "LoaderPackClut must be 12 bytes");
|
||||||
|
|
||||||
|
|
||||||
|
class LoadingScreen {
|
||||||
|
public:
|
||||||
|
/// Try to load a loader pack from a file.
|
||||||
|
/// Returns true if a loading screen was successfully loaded.
|
||||||
|
/// @param gpu GPU reference for rendering.
|
||||||
|
/// @param systemFont System font used if no custom font for text.
|
||||||
|
/// @param sceneIndex Scene index to derive the filename (scene_N.loading).
|
||||||
|
bool load(psyqo::GPU& gpu, psyqo::Font<>& systemFont, int sceneIndex);
|
||||||
|
|
||||||
|
/// Render all loading screen elements to BOTH framebuffers,
|
||||||
|
/// then FREE all loaded data. After this, only updateProgress works.
|
||||||
|
void renderInitialAndFree(psyqo::GPU& gpu);
|
||||||
|
|
||||||
|
/// Update the progress bar to the given percentage (0-100).
|
||||||
|
/// Redraws the progress bar rectangles in both framebuffers.
|
||||||
|
/// Safe after data is freed — uses only cached layout values.
|
||||||
|
void updateProgress(psyqo::GPU& gpu, uint8_t percent);
|
||||||
|
|
||||||
|
/// Returns true if a loading screen was loaded (even after data freed).
|
||||||
|
bool isActive() const { return m_active; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Render a filled rectangle at an absolute VRAM position.
|
||||||
|
void drawRect(psyqo::GPU& gpu, int16_t x, int16_t y, int16_t w, int16_t h,
|
||||||
|
uint8_t r, uint8_t g, uint8_t b);
|
||||||
|
|
||||||
|
/// Render an image element (two textured triangles).
|
||||||
|
/// Assumes DrawingOffset is already configured for the target buffer.
|
||||||
|
void drawImage(psyqo::GPU& gpu, int handle, int16_t x, int16_t y,
|
||||||
|
int16_t w, int16_t h, uint8_t r, uint8_t g, uint8_t b);
|
||||||
|
|
||||||
|
/// Render custom-font text via sendPrimitive (TPage + Sprite per glyph).
|
||||||
|
/// Assumes DrawingOffset is already configured for the target buffer.
|
||||||
|
void drawCustomText(psyqo::GPU& gpu, int handle, int16_t x, int16_t y,
|
||||||
|
uint8_t r, uint8_t g, uint8_t b);
|
||||||
|
|
||||||
|
/// Render ALL elements to a single framebuffer at the given VRAM Y offset.
|
||||||
|
void renderToBuffer(psyqo::GPU& gpu, int16_t yOffset);
|
||||||
|
|
||||||
|
/// Upload atlas/CLUT data to VRAM.
|
||||||
|
void uploadTextures(psyqo::GPU& gpu);
|
||||||
|
|
||||||
|
/// Find the "loading" progress bar element and cache its layout.
|
||||||
|
void findProgressBar();
|
||||||
|
|
||||||
|
uint8_t* m_data = nullptr;
|
||||||
|
int m_dataSize = 0;
|
||||||
|
psyqo::Font<>* m_font = nullptr;
|
||||||
|
bool m_active = false;
|
||||||
|
|
||||||
|
// Temporary UISystem to parse the loader pack's canvas/element data
|
||||||
|
UISystem m_ui;
|
||||||
|
|
||||||
|
// Cached layout for the "loading" progress bar (resolved at load time)
|
||||||
|
bool m_hasProgressBar = false;
|
||||||
|
int16_t m_barX = 0, m_barY = 0, m_barW = 0, m_barH = 0;
|
||||||
|
uint8_t m_barFillR = 255, m_barFillG = 255, m_barFillB = 255;
|
||||||
|
uint8_t m_barBgR = 0, m_barBgG = 0, m_barBgB = 0;
|
||||||
|
|
||||||
|
// Resolution from the loader pack
|
||||||
|
int16_t m_resW = 320, m_resH = 240;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
622
src/lua.cpp
Normal file
622
src/lua.cpp
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
#include <psyqo-lua/lua.hh>
|
||||||
|
|
||||||
|
#include <psyqo/alloc.h>
|
||||||
|
#include <psyqo/soft-math.hh>
|
||||||
|
#include <psyqo/trigonometry.hh>
|
||||||
|
#include <psyqo/xprintf.h>
|
||||||
|
|
||||||
|
#include "gameobject.hh"
|
||||||
|
|
||||||
|
// OOM-guarded allocator for Lua. The linker redirects luaI_realloc
|
||||||
|
// here instead of straight to psyqo_realloc, so we can log before
|
||||||
|
// returning NULL.
|
||||||
|
extern "C" void *lua_oom_realloc(void *ptr, size_t size) {
|
||||||
|
void *result = psyqo_realloc(ptr, size);
|
||||||
|
if (!result && size > 0) {
|
||||||
|
printf("Lua OOM: alloc %u bytes failed\n", (unsigned)size);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-compiled PS1 Lua bytecode for the GameObject metatable script.
|
||||||
|
// Compiled with luac_psx to avoid needing the Lua parser at runtime.
|
||||||
|
#include "gameobject_bytecode.h"
|
||||||
|
|
||||||
|
// Lua helpers
|
||||||
|
|
||||||
|
static constexpr int32_t kFixedScale = 4096;
|
||||||
|
|
||||||
|
// Accept FixedPoint object or plain number from Lua
|
||||||
|
static psyqo::FixedPoint<12> readFP(psyqo::Lua& L, int idx) {
|
||||||
|
if (L.isFixedPoint(idx)) return L.toFixedPoint(idx);
|
||||||
|
return psyqo::FixedPoint<12>(static_cast<int32_t>(L.toNumber(idx) * kFixedScale), psyqo::FixedPoint<12>::RAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int gameobjectGetPosition(psyqo::Lua L) {
|
||||||
|
|
||||||
|
auto go = L.toUserdata<psxsplash::GameObject>(1);
|
||||||
|
|
||||||
|
L.newTable();
|
||||||
|
L.push(go->position.x);
|
||||||
|
L.setField(2, "x");
|
||||||
|
L.push(go->position.y);
|
||||||
|
L.setField(2, "y");
|
||||||
|
L.push(go->position.z);
|
||||||
|
L.setField(2, "z");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static int gameobjectSetPosition(psyqo::Lua L) {
|
||||||
|
|
||||||
|
auto go = L.toUserdata<psxsplash::GameObject>(1);
|
||||||
|
|
||||||
|
L.getField(2, "x");
|
||||||
|
go->position.x = readFP(L, 3);
|
||||||
|
L.pop();
|
||||||
|
|
||||||
|
L.getField(2, "y");
|
||||||
|
go->position.y = readFP(L, 3);
|
||||||
|
L.pop();
|
||||||
|
|
||||||
|
L.getField(2, "z");
|
||||||
|
go->position.z = readFP(L, 3);
|
||||||
|
L.pop();
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static int gameobjectGetActive(psyqo::Lua L) {
|
||||||
|
auto go = L.toUserdata<psxsplash::GameObject>(1);
|
||||||
|
L.push(go->isActive());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int gameobjectSetActive(psyqo::Lua L) {
|
||||||
|
auto go = L.toUserdata<psxsplash::GameObject>(1);
|
||||||
|
bool active = L.toBoolean(2);
|
||||||
|
go->setActive(active);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static psyqo::Trig<> s_trig;
|
||||||
|
|
||||||
|
static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) {
|
||||||
|
psyqo::Angle result;
|
||||||
|
if (cosVal == 0 && sinVal == 0) { result.value = 0; return result; }
|
||||||
|
|
||||||
|
int32_t abs_s = sinVal < 0 ? -sinVal : sinVal;
|
||||||
|
int32_t abs_c = cosVal < 0 ? -cosVal : cosVal;
|
||||||
|
|
||||||
|
int32_t minV = abs_s < abs_c ? abs_s : abs_c;
|
||||||
|
int32_t maxV = abs_s > abs_c ? abs_s : abs_c;
|
||||||
|
int32_t angle = (minV * 256) / maxV;
|
||||||
|
|
||||||
|
if (abs_s > abs_c) angle = 512 - angle;
|
||||||
|
if (cosVal < 0) angle = 1024 - angle;
|
||||||
|
if (sinVal < 0) angle = -angle;
|
||||||
|
|
||||||
|
result.value = angle;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int gameobjectGetRotation(psyqo::Lua L) {
|
||||||
|
auto go = L.toUserdata<psxsplash::GameObject>(1);
|
||||||
|
// Decompose Y-axis rotation from the matrix (vs[0].x = cos, vs[0].z = sin)
|
||||||
|
// For full XYZ, we extract approximate Euler angles from the rotation matrix.
|
||||||
|
// Row 0: [cos(Y)cos(Z), -cos(Y)sin(Z), sin(Y)]
|
||||||
|
// This is a simplified extraction assuming common rotation order Y*X*Z.
|
||||||
|
int32_t sinY = go->rotation.vs[0].z.raw();
|
||||||
|
int32_t cosY = go->rotation.vs[0].x.raw();
|
||||||
|
int32_t sinX = -go->rotation.vs[1].z.raw();
|
||||||
|
int32_t cosX = go->rotation.vs[2].z.raw();
|
||||||
|
int32_t sinZ = -go->rotation.vs[0].y.raw();
|
||||||
|
int32_t cosZ = go->rotation.vs[0].x.raw();
|
||||||
|
|
||||||
|
auto toFP12 = [](psyqo::Angle a) -> psyqo::FixedPoint<12> {
|
||||||
|
psyqo::FixedPoint<12> fp;
|
||||||
|
fp.value = a.value << 2;
|
||||||
|
return fp;
|
||||||
|
};
|
||||||
|
|
||||||
|
L.newTable();
|
||||||
|
L.push(toFP12(fastAtan2(sinX, cosX)));
|
||||||
|
L.setField(2, "x");
|
||||||
|
L.push(toFP12(fastAtan2(sinY, cosY)));
|
||||||
|
L.setField(2, "y");
|
||||||
|
L.push(toFP12(fastAtan2(sinZ, cosZ)));
|
||||||
|
L.setField(2, "z");
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int gameobjectSetRotation(psyqo::Lua L) {
|
||||||
|
auto go = L.toUserdata<psxsplash::GameObject>(1);
|
||||||
|
|
||||||
|
L.getField(2, "x");
|
||||||
|
psyqo::FixedPoint<12> fpX = readFP(L, 3);
|
||||||
|
L.pop();
|
||||||
|
|
||||||
|
L.getField(2, "y");
|
||||||
|
psyqo::FixedPoint<12> fpY = readFP(L, 3);
|
||||||
|
L.pop();
|
||||||
|
|
||||||
|
L.getField(2, "z");
|
||||||
|
psyqo::FixedPoint<12> fpZ = readFP(L, 3);
|
||||||
|
L.pop();
|
||||||
|
|
||||||
|
// Convert FixedPoint<12> to Angle (FixedPoint<10>)
|
||||||
|
psyqo::Angle rx, ry, rz;
|
||||||
|
rx.value = fpX.value >> 2;
|
||||||
|
ry.value = fpY.value >> 2;
|
||||||
|
rz.value = fpZ.value >> 2;
|
||||||
|
|
||||||
|
// Compose Y * X * Z rotation matrix
|
||||||
|
auto matY = psyqo::SoftMath::generateRotationMatrix33(ry, psyqo::SoftMath::Axis::Y, s_trig);
|
||||||
|
auto matX = psyqo::SoftMath::generateRotationMatrix33(rx, psyqo::SoftMath::Axis::X, s_trig);
|
||||||
|
auto matZ = psyqo::SoftMath::generateRotationMatrix33(rz, psyqo::SoftMath::Axis::Z, s_trig);
|
||||||
|
auto temp = psyqo::SoftMath::multiplyMatrix33(matY, matX);
|
||||||
|
go->rotation = psyqo::SoftMath::multiplyMatrix33(temp, matZ);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int gameobjectGetRotationY(psyqo::Lua L) {
|
||||||
|
auto go = L.toUserdata<psxsplash::GameObject>(1);
|
||||||
|
int32_t sinRaw = go->rotation.vs[0].z.raw();
|
||||||
|
int32_t cosRaw = go->rotation.vs[0].x.raw();
|
||||||
|
psyqo::Angle angle = fastAtan2(sinRaw, cosRaw);
|
||||||
|
// Angle is FixedPoint<10> (pi-units). Convert to FixedPoint<12> for Lua.
|
||||||
|
psyqo::FixedPoint<12> fp12;
|
||||||
|
fp12.value = angle.value << 2;
|
||||||
|
L.push(fp12);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int gameobjectSetRotationY(psyqo::Lua L) {
|
||||||
|
auto go = L.toUserdata<psxsplash::GameObject>(1);
|
||||||
|
// Accept FixedPoint<12> from Lua, convert to Angle (FixedPoint<10>)
|
||||||
|
psyqo::FixedPoint<12> fp12 = readFP(L, 2);
|
||||||
|
psyqo::Angle angle;
|
||||||
|
angle.value = fp12.value >> 2;
|
||||||
|
go->rotation = psyqo::SoftMath::generateRotationMatrix33(angle, psyqo::SoftMath::Axis::Y, s_trig);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::Init() {
|
||||||
|
auto L = m_state;
|
||||||
|
// Load and run the game objects script
|
||||||
|
if (L.loadBuffer(reinterpret_cast<const char*>(GAMEOBJECT_BYTECODE), sizeof(GAMEOBJECT_BYTECODE), "bytecode:gameObjects") == 0) {
|
||||||
|
if (L.pcall(0, 1) == 0) {
|
||||||
|
// This will be our metatable
|
||||||
|
L.newTable();
|
||||||
|
|
||||||
|
L.push(gameobjectGetPosition);
|
||||||
|
L.setField(-2, "get_position");
|
||||||
|
|
||||||
|
L.push(gameobjectSetPosition);
|
||||||
|
L.setField(-2, "set_position");
|
||||||
|
|
||||||
|
L.push(gameobjectGetActive);
|
||||||
|
L.setField(-2, "get_active");
|
||||||
|
|
||||||
|
L.push(gameobjectSetActive);
|
||||||
|
L.setField(-2, "set_active");
|
||||||
|
|
||||||
|
L.push(gameobjectGetRotation);
|
||||||
|
L.setField(-2, "get_rotation");
|
||||||
|
|
||||||
|
L.push(gameobjectSetRotation);
|
||||||
|
L.setField(-2, "set_rotation");
|
||||||
|
|
||||||
|
L.push(gameobjectGetRotationY);
|
||||||
|
L.setField(-2, "get_rotationY");
|
||||||
|
|
||||||
|
L.push(gameobjectSetRotationY);
|
||||||
|
L.setField(-2, "set_rotationY");
|
||||||
|
|
||||||
|
L.copy(-1);
|
||||||
|
m_metatableReference = L.ref();
|
||||||
|
|
||||||
|
if (L.pcall(1, 0) == 0) {
|
||||||
|
// success
|
||||||
|
} else {
|
||||||
|
printf("Error registering Lua script: %s\n", L.optString(-1, "Unknown error"));
|
||||||
|
L.clearStack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Print Lua error if script execution fails
|
||||||
|
printf("Error executing Lua script: %s\n", L.optString(-1, "Unknown error"));
|
||||||
|
L.clearStack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Print Lua error if script loading fails
|
||||||
|
printf("Error loading Lua script: %s\n", L.optString(-1, "Unknown error"));
|
||||||
|
L.clearStack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
L.newTable();
|
||||||
|
m_luascriptsReference = L.ref();
|
||||||
|
|
||||||
|
// Add __concat to the FixedPoint metatable so FixedPoint values work with ..
|
||||||
|
// psyqo-lua doesn't provide this, but scripts need it for Debug.Log etc.
|
||||||
|
L.getField(LUA_REGISTRYINDEX, "psyqo.FixedPoint");
|
||||||
|
if (L.isTable(-1)) {
|
||||||
|
L.push([](psyqo::Lua L) -> int {
|
||||||
|
// Convert both operands to strings and concatenate
|
||||||
|
char buf[64];
|
||||||
|
int len = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i <= 2; i++) {
|
||||||
|
if (L.isFixedPoint(i)) {
|
||||||
|
auto fp = L.toFixedPoint(i);
|
||||||
|
int32_t raw = fp.raw();
|
||||||
|
int integer = raw >> 12;
|
||||||
|
unsigned fraction = (raw < 0 ? -raw : raw) & 0xfff;
|
||||||
|
if (fraction == 0) {
|
||||||
|
len += snprintf(buf + len, sizeof(buf) - len, "%d", integer);
|
||||||
|
} else {
|
||||||
|
unsigned decimal = (fraction * 1000) >> 12;
|
||||||
|
if (raw < 0 && integer == 0)
|
||||||
|
len += snprintf(buf + len, sizeof(buf) - len, "-%d.%03u", integer, decimal);
|
||||||
|
else
|
||||||
|
len += snprintf(buf + len, sizeof(buf) - len, "%d.%03u", integer, decimal);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const char* s = L.toString(i);
|
||||||
|
if (s) {
|
||||||
|
int slen = 0;
|
||||||
|
while (s[slen]) slen++;
|
||||||
|
if (len + slen < (int)sizeof(buf)) {
|
||||||
|
for (int j = 0; j < slen; j++) buf[len++] = s[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf[len] = '\0';
|
||||||
|
L.push(buf, len);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
L.setField(-2, "__concat");
|
||||||
|
}
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::Shutdown() {
|
||||||
|
|
||||||
|
if (m_state.getState()) {
|
||||||
|
m_state.close();
|
||||||
|
}
|
||||||
|
m_metatableReference = LUA_NOREF;
|
||||||
|
m_luascriptsReference = LUA_NOREF;
|
||||||
|
m_luaSceneScriptsReference = LUA_NOREF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::Reset() {
|
||||||
|
Shutdown();
|
||||||
|
m_state = psyqo::Lua();
|
||||||
|
Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::LoadLuaFile(const char* code, size_t len, int index) {
|
||||||
|
// Store bytecode reference for per-object re-execution in RegisterGameObject.
|
||||||
|
if (index < MAX_LUA_FILES) {
|
||||||
|
m_bytecodeRefs[index] = {code, len};
|
||||||
|
if (index >= m_bytecodeRefCount) m_bytecodeRefCount = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto L = m_state;
|
||||||
|
char filename[32];
|
||||||
|
snprintf(filename, sizeof(filename), "lua_asset:%d", index);
|
||||||
|
if (L.loadBuffer(code, len, filename) != LUA_OK) {
|
||||||
|
printf("Lua error: %s\n", L.toString(-1));
|
||||||
|
L.pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// (1) script func
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference);
|
||||||
|
// (1) script func (2) scripts table
|
||||||
|
L.newTable();
|
||||||
|
// (1) script func (2) scripts table (3) env {}
|
||||||
|
|
||||||
|
// Give the environment a metatable that falls back to _G
|
||||||
|
// so scripts can see Entity, Debug, Input, etc.
|
||||||
|
L.newTable();
|
||||||
|
// (1) script func (2) scripts table (3) env {} (4) mt {}
|
||||||
|
L.pushGlobalTable();
|
||||||
|
// (1) script func (2) scripts table (3) env {} (4) mt {} (5) _G
|
||||||
|
L.setField(-2, "__index");
|
||||||
|
// (1) script func (2) scripts table (3) env {} (4) mt { __index = _G }
|
||||||
|
L.setMetatable(-2);
|
||||||
|
// (1) script func (2) scripts table (3) env { mt }
|
||||||
|
|
||||||
|
L.pushNumber(index);
|
||||||
|
// (1) script func (2) scripts table (3) env (4) index
|
||||||
|
L.copy(-2);
|
||||||
|
// (1) script func (2) scripts table (3) env (4) index (5) env
|
||||||
|
L.setTable(-4);
|
||||||
|
// (1) script func (2) scripts table (3) env
|
||||||
|
lua_setupvalue(L.getState(), -3, 1);
|
||||||
|
// (1) script func (2) scripts table
|
||||||
|
L.pop();
|
||||||
|
// (1) script func
|
||||||
|
if (L.pcall(0, 0)) {
|
||||||
|
printf("Lua error: %s\n", L.toString(-1));
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::RegisterSceneScripts(int index) {
|
||||||
|
if (index < 0) return;
|
||||||
|
auto L = m_state;
|
||||||
|
L.newTable();
|
||||||
|
// (1) {}
|
||||||
|
L.copy(1);
|
||||||
|
// (1) {} (2) {}
|
||||||
|
m_luaSceneScriptsReference = L.ref();
|
||||||
|
// (1) {}
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference);
|
||||||
|
// (1) {} (2) scripts table
|
||||||
|
L.pushNumber(index);
|
||||||
|
// (1) {} (2) script environments table (3) index
|
||||||
|
L.getTable(-2);
|
||||||
|
// (1) {} (2) script environments table (3) script environment table for the scene
|
||||||
|
if (!L.isTable(-1)) {
|
||||||
|
// Scene Lua file index is invalid or script not loaded
|
||||||
|
printf("Warning: scene Lua file index %d not found\n", index);
|
||||||
|
L.pop(3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSceneCreationStartFunctionWrapper.resolveGlobal(L);
|
||||||
|
onSceneCreationEndFunctionWrapper.resolveGlobal(L);
|
||||||
|
L.pop(3);
|
||||||
|
// empty stack
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::RegisterGameObject(GameObject* go) {
|
||||||
|
uint8_t* ptr = reinterpret_cast<uint8_t*>(go);
|
||||||
|
auto L = m_state;
|
||||||
|
L.push(ptr);
|
||||||
|
// (1) go
|
||||||
|
L.newTable();
|
||||||
|
// (1) go (2) {}
|
||||||
|
L.push(ptr);
|
||||||
|
// (1) go (2) {} (3) go
|
||||||
|
L.setField(-2, "__cpp_ptr");
|
||||||
|
// (1) go (2) { __cpp_ptr = go }
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, m_metatableReference);
|
||||||
|
// (1) go (2) { __cpp_ptr = go } (3) metatable
|
||||||
|
if (L.isTable(-1)) {
|
||||||
|
L.setMetatable(-2);
|
||||||
|
} else {
|
||||||
|
printf("Warning: metatableForAllGameObjects not found\n");
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
// (1) go (2) { __cpp_ptr = go + metatable }
|
||||||
|
L.rawSet(LUA_REGISTRYINDEX);
|
||||||
|
// empty stack
|
||||||
|
L.newTable();
|
||||||
|
// (1) {}
|
||||||
|
L.push(ptr + 1);
|
||||||
|
// (1) {} (2) go + 1
|
||||||
|
L.copy(1);
|
||||||
|
// (1) {} (2) go + 1 (3) {}
|
||||||
|
L.rawSet(LUA_REGISTRYINDEX);
|
||||||
|
// (1) {}
|
||||||
|
|
||||||
|
// Initialize event mask for this object
|
||||||
|
uint32_t eventMask = EVENT_NONE;
|
||||||
|
|
||||||
|
if (go->luaFileIndex != -1 && go->luaFileIndex < m_bytecodeRefCount) {
|
||||||
|
auto& ref = m_bytecodeRefs[go->luaFileIndex];
|
||||||
|
char filename[32];
|
||||||
|
snprintf(filename, sizeof(filename), "lua_asset:%d", go->luaFileIndex);
|
||||||
|
|
||||||
|
if (L.loadBuffer(ref.code, ref.len, filename) == LUA_OK) {
|
||||||
|
// (1) method_table (2) chunk_func
|
||||||
|
|
||||||
|
// Create a per-object environment with __index = _G
|
||||||
|
// so this object's file-level locals are isolated.
|
||||||
|
L.newTable();
|
||||||
|
L.newTable();
|
||||||
|
L.pushGlobalTable();
|
||||||
|
L.setField(-2, "__index");
|
||||||
|
L.setMetatable(-2);
|
||||||
|
// (1) method_table (2) chunk_func (3) env
|
||||||
|
|
||||||
|
// Set env as the chunk's _ENV upvalue
|
||||||
|
L.copy(-1);
|
||||||
|
// (1) method_table (2) chunk_func (3) env (4) env_copy
|
||||||
|
lua_setupvalue(L.getState(), -3, 1);
|
||||||
|
// (1) method_table (2) chunk_func (3) env
|
||||||
|
|
||||||
|
// Move chunk to top for pcall
|
||||||
|
lua_insert(L.getState(), -2);
|
||||||
|
// (1) method_table (2) env (3) chunk_func
|
||||||
|
|
||||||
|
if (L.pcall(0, 0) == LUA_OK) {
|
||||||
|
// (1) method_table (2) env
|
||||||
|
// resolveGlobal expects: (1) method_table, (3) env
|
||||||
|
// Insert a placeholder at position 2 to push env to position 3
|
||||||
|
L.push(); // push nil
|
||||||
|
// (1) method_table (2) env (3) nil
|
||||||
|
lua_insert(L.getState(), 2);
|
||||||
|
// (1) method_table (2) nil (3) env
|
||||||
|
|
||||||
|
// Resolve each event - creates fresh function refs with isolated upvalues
|
||||||
|
if (onCreateMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_CREATE;
|
||||||
|
if (onCollideWithPlayerMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_COLLISION;
|
||||||
|
if (onInteractMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_INTERACT;
|
||||||
|
if (onTriggerEnterMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_TRIGGER_ENTER;
|
||||||
|
if (onTriggerExitMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_TRIGGER_EXIT;
|
||||||
|
if (onUpdateMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_UPDATE;
|
||||||
|
if (onDestroyMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_DESTROY;
|
||||||
|
if (onEnableMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_ENABLE;
|
||||||
|
if (onDisableMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_DISABLE;
|
||||||
|
if (onButtonPressMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_BUTTON_PRESS;
|
||||||
|
if (onButtonReleaseMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_BUTTON_RELEASE;
|
||||||
|
|
||||||
|
L.pop(2); // pop nil and env
|
||||||
|
} else {
|
||||||
|
printf("Lua error: %s\n", L.toString(-1));
|
||||||
|
L.pop(2); // pop error msg and env
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printf("Lua error: %s\n", L.toString(-1));
|
||||||
|
L.pop(); // pop error msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the event mask directly in the GameObject
|
||||||
|
go->eventMask = eventMask;
|
||||||
|
|
||||||
|
L.pop();
|
||||||
|
// empty stack
|
||||||
|
// Note: onCreate is NOT fired here. Call FireAllOnCreate() after all objects
|
||||||
|
// are registered so that Entity.Find works across all objects in onCreate.
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::FireAllOnCreate(GameObject** objects, size_t count) {
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
if (objects[i] && (objects[i]->eventMask & EVENT_ON_CREATE)) {
|
||||||
|
onCreateMethodWrapper.callMethod(*this, objects[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnCollideWithPlayer(GameObject* self) {
|
||||||
|
if (!hasEvent(self, EVENT_ON_COLLISION)) return;
|
||||||
|
onCollideWithPlayerMethodWrapper.callMethod(*this, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnInteract(GameObject* self) {
|
||||||
|
if (!hasEvent(self, EVENT_ON_INTERACT)) return;
|
||||||
|
onInteractMethodWrapper.callMethod(*this, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnTriggerEnter(GameObject* trigger, GameObject* other) {
|
||||||
|
if (!hasEvent(trigger, EVENT_ON_TRIGGER_ENTER)) return;
|
||||||
|
onTriggerEnterMethodWrapper.callMethod(*this, trigger, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnTriggerExit(GameObject* trigger, GameObject* other) {
|
||||||
|
if (!hasEvent(trigger, EVENT_ON_TRIGGER_EXIT)) return;
|
||||||
|
onTriggerExitMethodWrapper.callMethod(*this, trigger, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnTriggerEnterScript(int luaFileIndex, int triggerIndex) {
|
||||||
|
auto L = m_state;
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference);
|
||||||
|
L.rawGetI(-1, luaFileIndex);
|
||||||
|
if (!L.isTable(-1)) { L.clearStack(); return; }
|
||||||
|
L.push("onTriggerEnter", 14);
|
||||||
|
L.getTable(-2);
|
||||||
|
if (!L.isFunction(-1)) { L.clearStack(); return; }
|
||||||
|
L.pushNumber(triggerIndex);
|
||||||
|
if (L.pcall(1, 0) != LUA_OK) {
|
||||||
|
printf("Lua error: %s\n", L.toString(-1));
|
||||||
|
}
|
||||||
|
L.clearStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnTriggerExitScript(int luaFileIndex, int triggerIndex) {
|
||||||
|
auto L = m_state;
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference);
|
||||||
|
L.rawGetI(-1, luaFileIndex);
|
||||||
|
if (!L.isTable(-1)) { L.clearStack(); return; }
|
||||||
|
L.push("onTriggerExit", 13);
|
||||||
|
L.getTable(-2);
|
||||||
|
if (!L.isFunction(-1)) { L.clearStack(); return; }
|
||||||
|
L.pushNumber(triggerIndex);
|
||||||
|
if (L.pcall(1, 0) != LUA_OK) {
|
||||||
|
printf("Lua error: %s\n", L.toString(-1));
|
||||||
|
}
|
||||||
|
L.clearStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnDestroy(GameObject* go) {
|
||||||
|
if (!hasEvent(go, EVENT_ON_DESTROY)) return;
|
||||||
|
onDestroyMethodWrapper.callMethod(*this, go);
|
||||||
|
go->eventMask = EVENT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnEnable(GameObject* go) {
|
||||||
|
if (!hasEvent(go, EVENT_ON_ENABLE)) return;
|
||||||
|
onEnableMethodWrapper.callMethod(*this, go);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnDisable(GameObject* go) {
|
||||||
|
if (!hasEvent(go, EVENT_ON_DISABLE)) return;
|
||||||
|
onDisableMethodWrapper.callMethod(*this, go);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnButtonPress(GameObject* go, int button) {
|
||||||
|
if (!hasEvent(go, EVENT_ON_BUTTON_PRESS)) return;
|
||||||
|
onButtonPressMethodWrapper.callMethod(*this, go, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnButtonRelease(GameObject* go, int button) {
|
||||||
|
if (!hasEvent(go, EVENT_ON_BUTTON_RELEASE)) return;
|
||||||
|
onButtonReleaseMethodWrapper.callMethod(*this, go, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::OnUpdate(GameObject* go, int deltaFrames) {
|
||||||
|
if (!hasEvent(go, EVENT_ON_UPDATE)) return;
|
||||||
|
onUpdateMethodWrapper.callMethod(*this, go, deltaFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::RelocateGameObjects(GameObject** objects, size_t count, intptr_t delta) {
|
||||||
|
auto L = m_state;
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
uint8_t* newPtr = reinterpret_cast<uint8_t*>(objects[i]);
|
||||||
|
uint8_t* oldPtr = newPtr - delta;
|
||||||
|
|
||||||
|
// Re-key the main game object table: registry[oldPtr] -> registry[newPtr]
|
||||||
|
L.push(oldPtr);
|
||||||
|
L.rawGet(LUA_REGISTRYINDEX);
|
||||||
|
if (L.isTable(-1)) {
|
||||||
|
// Update __cpp_ptr inside the table
|
||||||
|
L.push(newPtr);
|
||||||
|
L.setField(-2, "__cpp_ptr");
|
||||||
|
// Store at new key
|
||||||
|
L.push(newPtr);
|
||||||
|
L.copy(-2);
|
||||||
|
L.rawSet(LUA_REGISTRYINDEX);
|
||||||
|
// Remove old key
|
||||||
|
L.push(oldPtr);
|
||||||
|
L.push(); // nil
|
||||||
|
L.rawSet(LUA_REGISTRYINDEX);
|
||||||
|
}
|
||||||
|
L.pop();
|
||||||
|
|
||||||
|
// Re-key the methods table: registry[oldPtr+1] -> registry[newPtr+1]
|
||||||
|
L.push(oldPtr + 1);
|
||||||
|
L.rawGet(LUA_REGISTRYINDEX);
|
||||||
|
if (L.isTable(-1)) {
|
||||||
|
L.push(newPtr + 1);
|
||||||
|
L.copy(-2);
|
||||||
|
L.rawSet(LUA_REGISTRYINDEX);
|
||||||
|
L.push(oldPtr + 1);
|
||||||
|
L.push();
|
||||||
|
L.rawSet(LUA_REGISTRYINDEX);
|
||||||
|
}
|
||||||
|
L.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Lua::PushGameObject(GameObject* go) {
|
||||||
|
auto L = m_state;
|
||||||
|
L.push(go);
|
||||||
|
L.rawGet(LUA_REGISTRYINDEX);
|
||||||
|
|
||||||
|
if (!L.isTable(-1)) {
|
||||||
|
L.pop();
|
||||||
|
L.push(); // push nil so the caller always gets a value
|
||||||
|
}
|
||||||
|
}
|
||||||
197
src/lua.h
Normal file
197
src/lua.h
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "gameobject.hh"
|
||||||
|
#include "psyqo-lua/lua.hh"
|
||||||
|
#include "psyqo/xprintf.h"
|
||||||
|
#include "typestring.h"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
struct LuaFile {
|
||||||
|
union {
|
||||||
|
uint32_t luaCodeOffset;
|
||||||
|
const char* luaCode;
|
||||||
|
};
|
||||||
|
uint32_t length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event bitmask flags - each bit represents whether an object handles that event.
|
||||||
|
* This allows O(1) checking before calling into Lua VM.
|
||||||
|
*
|
||||||
|
* CRITICAL: The PS1 cannot afford to call into Lua for events objects don't handle.
|
||||||
|
* When registering a GameObject, we scan its script and set these bits.
|
||||||
|
* During dispatch, we check the bit FIRST before any Lua VM access.
|
||||||
|
*/
|
||||||
|
enum EventMask : uint32_t {
|
||||||
|
EVENT_NONE = 0,
|
||||||
|
EVENT_ON_CREATE = 1 << 0,
|
||||||
|
EVENT_ON_COLLISION = 1 << 1,
|
||||||
|
EVENT_ON_INTERACT = 1 << 2,
|
||||||
|
EVENT_ON_TRIGGER_ENTER = 1 << 3,
|
||||||
|
EVENT_ON_TRIGGER_EXIT = 1 << 4,
|
||||||
|
EVENT_ON_UPDATE = 1 << 5,
|
||||||
|
EVENT_ON_DESTROY = 1 << 6,
|
||||||
|
EVENT_ON_ENABLE = 1 << 7,
|
||||||
|
EVENT_ON_DISABLE = 1 << 8,
|
||||||
|
EVENT_ON_BUTTON_PRESS = 1 << 9,
|
||||||
|
EVENT_ON_BUTTON_RELEASE = 1 << 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Lua {
|
||||||
|
public:
|
||||||
|
void Init();
|
||||||
|
void Reset(); // Destroy and recreate the Lua VM (call on scene load)
|
||||||
|
void Shutdown(); // Close the Lua VM without recreating (call on scene unload)
|
||||||
|
|
||||||
|
void LoadLuaFile(const char* code, size_t len, int index);
|
||||||
|
void RegisterSceneScripts(int index);
|
||||||
|
void RegisterGameObject(GameObject* go);
|
||||||
|
void FireAllOnCreate(GameObject** objects, size_t count);
|
||||||
|
void RelocateGameObjects(GameObject** objects, size_t count, intptr_t delta);
|
||||||
|
|
||||||
|
// Get the underlying psyqo::Lua state for API registration
|
||||||
|
psyqo::Lua& getState() { return m_state; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a GameObject handles a specific event.
|
||||||
|
* Call this BEFORE attempting to dispatch any event.
|
||||||
|
*/
|
||||||
|
bool hasEvent(GameObject* go, EventMask event) const {
|
||||||
|
return (go->eventMask & event) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnSceneCreationStart() {
|
||||||
|
onSceneCreationStartFunctionWrapper.callFunction(*this);
|
||||||
|
}
|
||||||
|
void OnSceneCreationEnd() {
|
||||||
|
onSceneCreationEndFunctionWrapper.callFunction(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event dispatchers - these check the bitmask before calling Lua
|
||||||
|
void OnCollideWithPlayer(GameObject* self);
|
||||||
|
void OnInteract(GameObject* self);
|
||||||
|
void OnTriggerEnter(GameObject* trigger, GameObject* other);
|
||||||
|
void OnTriggerExit(GameObject* trigger, GameObject* other);
|
||||||
|
void OnTriggerEnterScript(int luaFileIndex, int triggerIndex);
|
||||||
|
void OnTriggerExitScript(int luaFileIndex, int triggerIndex);
|
||||||
|
void OnUpdate(GameObject* go, int deltaFrames); // Per-object update
|
||||||
|
void OnDestroy(GameObject* go);
|
||||||
|
void OnEnable(GameObject* go);
|
||||||
|
void OnDisable(GameObject* go);
|
||||||
|
void OnButtonPress(GameObject* go, int button);
|
||||||
|
void OnButtonRelease(GameObject* go, int button);
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <int methodId, typename methodName>
|
||||||
|
struct FunctionWrapper;
|
||||||
|
template <int methodId, char... C>
|
||||||
|
struct FunctionWrapper<methodId, irqus::typestring<C...>> {
|
||||||
|
typedef irqus::typestring<C...> methodName;
|
||||||
|
|
||||||
|
// Returns true if the function was found and stored
|
||||||
|
static bool resolveGlobal(psyqo::Lua L) {
|
||||||
|
L.push(methodName::data(), methodName::size());
|
||||||
|
L.getTable(3);
|
||||||
|
|
||||||
|
if (L.isFunction(-1)) {
|
||||||
|
L.pushNumber(methodId);
|
||||||
|
L.copy(-2);
|
||||||
|
L.setTable(1);
|
||||||
|
L.pop(); // Pop the function
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
L.pop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
static void pushArgs(psxsplash::Lua& lua, Args... args) {
|
||||||
|
(push(lua, args), ...);
|
||||||
|
}
|
||||||
|
static void push(psxsplash::Lua& lua, GameObject* go) { lua.PushGameObject(go); }
|
||||||
|
static void push(psxsplash::Lua& lua, int val) { lua.m_state.pushNumber(val); }
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
static void callMethod(psxsplash::Lua& lua, GameObject* go, Args... args) {
|
||||||
|
auto L = lua.m_state;
|
||||||
|
uint8_t* ptr = reinterpret_cast<uint8_t*>(go);
|
||||||
|
L.push(ptr + 1);
|
||||||
|
L.rawGet(LUA_REGISTRYINDEX);
|
||||||
|
L.rawGetI(-1, methodId);
|
||||||
|
if (!L.isFunction(-1)) {
|
||||||
|
L.clearStack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lua.PushGameObject(go);
|
||||||
|
pushArgs(lua, args...);
|
||||||
|
if (L.pcall(sizeof...(Args) + 1, 0) != LUA_OK) {
|
||||||
|
printf("Lua error: %s\n", L.toString(-1));
|
||||||
|
}
|
||||||
|
L.clearStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
static void callFunction(psxsplash::Lua& lua, Args... args) {
|
||||||
|
auto L = lua.m_state;
|
||||||
|
L.rawGetI(LUA_REGISTRYINDEX, lua.m_luaSceneScriptsReference);
|
||||||
|
if (!L.isTable(-1)) {
|
||||||
|
L.clearStack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
L.rawGetI(-1, methodId);
|
||||||
|
if (!L.isFunction(-1)) {
|
||||||
|
L.clearStack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushArgs(lua, args...);
|
||||||
|
if (L.pcall(sizeof...(Args), 0) != LUA_OK) {
|
||||||
|
printf("Lua error: %s\n", L.toString(-1));
|
||||||
|
}
|
||||||
|
L.clearStack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scene-level events (methodId 1-2)
|
||||||
|
[[no_unique_address]] FunctionWrapper<1, typestring_is("onSceneCreationStart")> onSceneCreationStartFunctionWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<2, typestring_is("onSceneCreationEnd")> onSceneCreationEndFunctionWrapper;
|
||||||
|
|
||||||
|
// Object-level events
|
||||||
|
[[no_unique_address]] FunctionWrapper<100, typestring_is("onCreate")> onCreateMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<101, typestring_is("onCollideWithPlayer")> onCollideWithPlayerMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<102, typestring_is("onInteract")> onInteractMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<103, typestring_is("onTriggerEnter")> onTriggerEnterMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<104, typestring_is("onTriggerExit")> onTriggerExitMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<105, typestring_is("onUpdate")> onUpdateMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<106, typestring_is("onDestroy")> onDestroyMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<107, typestring_is("onEnable")> onEnableMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<108, typestring_is("onDisable")> onDisableMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<109, typestring_is("onButtonPress")> onButtonPressMethodWrapper;
|
||||||
|
[[no_unique_address]] FunctionWrapper<110, typestring_is("onButtonRelease")> onButtonReleaseMethodWrapper;
|
||||||
|
|
||||||
|
void PushGameObject(GameObject* go);
|
||||||
|
|
||||||
|
private:
|
||||||
|
psyqo::Lua m_state;
|
||||||
|
|
||||||
|
int m_metatableReference = LUA_NOREF;
|
||||||
|
int m_luascriptsReference = LUA_NOREF;
|
||||||
|
int m_luaSceneScriptsReference = LUA_NOREF;
|
||||||
|
|
||||||
|
// Bytecode references for per-object re-execution.
|
||||||
|
// Points into splashpack data which stays in memory for the scene lifetime.
|
||||||
|
static constexpr int MAX_LUA_FILES = 32;
|
||||||
|
struct BytecodeRef {
|
||||||
|
const char* code;
|
||||||
|
size_t len;
|
||||||
|
};
|
||||||
|
BytecodeRef m_bytecodeRefs[MAX_LUA_FILES];
|
||||||
|
int m_bytecodeRefCount = 0;
|
||||||
|
|
||||||
|
template <int methodId, typename methodName>
|
||||||
|
friend struct FunctionWrapper;
|
||||||
|
};
|
||||||
|
} // namespace psxsplash
|
||||||
1970
src/luaapi.cpp
Normal file
1970
src/luaapi.cpp
Normal file
File diff suppressed because it is too large
Load Diff
343
src/luaapi.hh
Normal file
343
src/luaapi.hh
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <psyqo-lua/lua.hh>
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
class SceneManager; // Forward declaration
|
||||||
|
class CutscenePlayer; // Forward declaration
|
||||||
|
class AnimationPlayer; // Forward declaration
|
||||||
|
class UISystem; // Forward declaration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lua API - Provides game scripting functionality
|
||||||
|
*
|
||||||
|
* Available namespaces:
|
||||||
|
* - Entity: Object finding, spawning, destruction
|
||||||
|
* - Vec3: Vector math operations
|
||||||
|
* - Input: Controller state queries
|
||||||
|
* - Timer: Timer control
|
||||||
|
* - Camera: Camera manipulation
|
||||||
|
* - Audio: Sound playback (future)
|
||||||
|
* - Scene: Scene management
|
||||||
|
*/
|
||||||
|
class LuaAPI {
|
||||||
|
public:
|
||||||
|
// Initialize all API modules
|
||||||
|
static void RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cutscenePlayer = nullptr, AnimationPlayer* animationPlayer = nullptr, UISystem* uiSystem = nullptr);
|
||||||
|
|
||||||
|
// Called once per frame to advance the Lua frame counter
|
||||||
|
static void IncrementFrameCount();
|
||||||
|
|
||||||
|
// Reset frame counter (called on scene load)
|
||||||
|
static void ResetFrameCount();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Store scene manager for API access
|
||||||
|
static SceneManager* s_sceneManager;
|
||||||
|
|
||||||
|
// Cutscene player pointer (set during RegisterAll)
|
||||||
|
static CutscenePlayer* s_cutscenePlayer;
|
||||||
|
|
||||||
|
// Animation player pointer (set during RegisterAll)
|
||||||
|
static AnimationPlayer* s_animationPlayer;
|
||||||
|
|
||||||
|
// UI system pointer (set during RegisterAll)
|
||||||
|
static UISystem* s_uiSystem;
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// ENTITY API
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Entity.FindByScriptIndex(index) -> object or nil
|
||||||
|
// Finds first object with matching Lua script file index
|
||||||
|
static int Entity_FindByScriptIndex(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.FindByIndex(index) -> object or nil
|
||||||
|
// Gets object by its array index
|
||||||
|
static int Entity_FindByIndex(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.Find(name) -> object or nil
|
||||||
|
// Finds first object with matching name (user-friendly)
|
||||||
|
static int Entity_Find(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.GetCount() -> number
|
||||||
|
// Returns total number of game objects
|
||||||
|
static int Entity_GetCount(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.SetActive(object, active)
|
||||||
|
// Sets object active state (fires onEnable/onDisable)
|
||||||
|
static int Entity_SetActive(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.IsActive(object) -> boolean
|
||||||
|
static int Entity_IsActive(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.GetPosition(object) -> {x, y, z}
|
||||||
|
static int Entity_GetPosition(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.SetPosition(object, {x, y, z})
|
||||||
|
static int Entity_SetPosition(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.GetRotationY(object) -> number (radians)
|
||||||
|
static int Entity_GetRotationY(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.SetRotationY(object, angle) -> nil
|
||||||
|
static int Entity_SetRotationY(lua_State* L);
|
||||||
|
|
||||||
|
// Entity.ForEach(callback) -> nil
|
||||||
|
// Calls callback(object, index) for each active game object
|
||||||
|
static int Entity_ForEach(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// VEC3 API - Vector math
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Vec3.new(x, y, z) -> {x, y, z}
|
||||||
|
static int Vec3_New(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.add(a, b) -> {x, y, z}
|
||||||
|
static int Vec3_Add(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.sub(a, b) -> {x, y, z}
|
||||||
|
static int Vec3_Sub(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.mul(v, scalar) -> {x, y, z}
|
||||||
|
static int Vec3_Mul(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.dot(a, b) -> number
|
||||||
|
static int Vec3_Dot(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.cross(a, b) -> {x, y, z}
|
||||||
|
static int Vec3_Cross(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.length(v) -> number
|
||||||
|
static int Vec3_Length(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.lengthSq(v) -> number (faster, no sqrt)
|
||||||
|
static int Vec3_LengthSq(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.normalize(v) -> {x, y, z}
|
||||||
|
static int Vec3_Normalize(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.distance(a, b) -> number
|
||||||
|
static int Vec3_Distance(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.distanceSq(a, b) -> number (faster)
|
||||||
|
static int Vec3_DistanceSq(lua_State* L);
|
||||||
|
|
||||||
|
// Vec3.lerp(a, b, t) -> {x, y, z}
|
||||||
|
static int Vec3_Lerp(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// INPUT API - Controller state
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Input.IsPressed(button) -> boolean
|
||||||
|
// True only on the frame the button was pressed
|
||||||
|
static int Input_IsPressed(lua_State* L);
|
||||||
|
|
||||||
|
// Input.IsReleased(button) -> boolean
|
||||||
|
// True only on the frame the button was released
|
||||||
|
static int Input_IsReleased(lua_State* L);
|
||||||
|
|
||||||
|
// Input.IsHeld(button) -> boolean
|
||||||
|
// True while the button is held down
|
||||||
|
static int Input_IsHeld(lua_State* L);
|
||||||
|
|
||||||
|
// Input.GetAnalog(stick) -> x, y
|
||||||
|
// Returns analog stick values (-128 to 127)
|
||||||
|
static int Input_GetAnalog(lua_State* L);
|
||||||
|
|
||||||
|
// Button constants (registered as Input.CROSS, Input.CIRCLE, etc.)
|
||||||
|
static void RegisterInputConstants(psyqo::Lua& L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// TIMER API - Frame counter
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Timer.GetFrameCount() -> number
|
||||||
|
// Returns total frames since scene start
|
||||||
|
static int Timer_GetFrameCount(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// CAMERA API - Camera control
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Camera.GetPosition() -> {x, y, z}
|
||||||
|
static int Camera_GetPosition(lua_State* L);
|
||||||
|
|
||||||
|
// Camera.SetPosition(x, y, z)
|
||||||
|
static int Camera_SetPosition(lua_State* L);
|
||||||
|
|
||||||
|
// Camera.GetRotation() -> {x, y, z}
|
||||||
|
static int Camera_GetRotation(lua_State* L);
|
||||||
|
|
||||||
|
// Camera.SetRotation(x, y, z)
|
||||||
|
static int Camera_SetRotation(lua_State* L);
|
||||||
|
|
||||||
|
// Camera.LookAt(target) or Camera.LookAt(x, y, z)
|
||||||
|
static int Camera_LookAt(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// AUDIO API - Sound playback (placeholder for SPU)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Audio.Play(soundId, volume, pan) -> channelId
|
||||||
|
// soundId can be a number (clip index) or string (clip name)
|
||||||
|
static int Audio_Play(lua_State* L);
|
||||||
|
|
||||||
|
// Audio.Find(name) -> clipIndex or nil
|
||||||
|
// Finds audio clip by name, returns its index for use with Play/Stop/etc.
|
||||||
|
static int Audio_Find(lua_State* L);
|
||||||
|
|
||||||
|
// Audio.Stop(channelId)
|
||||||
|
static int Audio_Stop(lua_State* L);
|
||||||
|
|
||||||
|
// Audio.SetVolume(channelId, volume)
|
||||||
|
static int Audio_SetVolume(lua_State* L);
|
||||||
|
|
||||||
|
// Audio.StopAll()
|
||||||
|
static int Audio_StopAll(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// DEBUG API - Development helpers
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Debug.Log(message)
|
||||||
|
static int Debug_Log(lua_State* L);
|
||||||
|
|
||||||
|
// Debug.DrawLine(start, end, color) - draws debug line next frame
|
||||||
|
static int Debug_DrawLine(lua_State* L);
|
||||||
|
|
||||||
|
// Debug.DrawBox(center, size, color)
|
||||||
|
static int Debug_DrawBox(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// MATH API - Additional math functions
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Math.Clamp(value, min, max)
|
||||||
|
static int Math_Clamp(lua_State* L);
|
||||||
|
|
||||||
|
// Math.Lerp(a, b, t)
|
||||||
|
static int Math_Lerp(lua_State* L);
|
||||||
|
|
||||||
|
// Math.Sign(value)
|
||||||
|
static int Math_Sign(lua_State* L);
|
||||||
|
|
||||||
|
// Math.Abs(value)
|
||||||
|
static int Math_Abs(lua_State* L);
|
||||||
|
|
||||||
|
// Math.Min(a, b)
|
||||||
|
static int Math_Min(lua_State* L);
|
||||||
|
|
||||||
|
// Math.Max(a, b)
|
||||||
|
static int Math_Max(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// SCENE API - Scene management
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Scene.Load(sceneIndex)
|
||||||
|
// Requests a scene transition to the given index (0-based).
|
||||||
|
// The actual load happens at the end of the current frame.
|
||||||
|
static int Scene_Load(lua_State* L);
|
||||||
|
|
||||||
|
// Scene.GetIndex() -> number
|
||||||
|
// Returns the index of the currently loaded scene.
|
||||||
|
static int Scene_GetIndex(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// PERSIST API - Data that survives scene loads
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Persist.Get(key) -> number or nil
|
||||||
|
static int Persist_Get(lua_State* L);
|
||||||
|
|
||||||
|
// Persist.Set(key, value)
|
||||||
|
static int Persist_Set(lua_State* L);
|
||||||
|
|
||||||
|
// Reset all persistent data
|
||||||
|
static void PersistClear();
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// CUTSCENE API - Cutscene playback control
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Cutscene.Play(name) or Cutscene.Play(name, {loop=bool, onComplete=fn})
|
||||||
|
static int Cutscene_Play(lua_State* L);
|
||||||
|
|
||||||
|
// Cutscene.Stop() -> nil
|
||||||
|
static int Cutscene_Stop(lua_State* L);
|
||||||
|
|
||||||
|
// Cutscene.IsPlaying() -> boolean
|
||||||
|
static int Cutscene_IsPlaying(lua_State* L);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// ANIMATION API - Multi-instance animation playback
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Animation.Play(name) or Animation.Play(name, {loop=bool, onComplete=fn})
|
||||||
|
static int Animation_Play(lua_State* L);
|
||||||
|
|
||||||
|
// Animation.Stop(name) -> nil
|
||||||
|
static int Animation_Stop(lua_State* L);
|
||||||
|
|
||||||
|
// Animation.IsPlaying(name) -> boolean
|
||||||
|
static int Animation_IsPlaying(lua_State* L);
|
||||||
|
|
||||||
|
// Controls.SetEnabled(bool) - enable/disable all player input
|
||||||
|
static int Controls_SetEnabled(lua_State* L);
|
||||||
|
|
||||||
|
// Controls.IsEnabled() -> boolean
|
||||||
|
static int Controls_IsEnabled(lua_State* L);
|
||||||
|
|
||||||
|
// Interact.SetEnabled(entity, bool) - enable/disable interaction + prompt for an object
|
||||||
|
static int Interact_SetEnabled(lua_State* L);
|
||||||
|
|
||||||
|
// Interact.IsEnabled(entity) -> boolean
|
||||||
|
static int Interact_IsEnabled(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
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// Push a Vec3 table onto the stack
|
||||||
|
static void PushVec3(psyqo::Lua& L, psyqo::FixedPoint<12> x,
|
||||||
|
psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z);
|
||||||
|
|
||||||
|
// Read a Vec3 table from the stack
|
||||||
|
static void ReadVec3(psyqo::Lua& L, int idx,
|
||||||
|
psyqo::FixedPoint<12>& x,
|
||||||
|
psyqo::FixedPoint<12>& y,
|
||||||
|
psyqo::FixedPoint<12>& z);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
151
src/main.cpp
151
src/main.cpp
@@ -1,52 +1,45 @@
|
|||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <psyqo/advancedpad.hh>
|
#include <psyqo/advancedpad.hh>
|
||||||
#include <psyqo/application.hh>
|
#include <psyqo/application.hh>
|
||||||
#include <psyqo/fixed-point.hh>
|
#include <psyqo/fixed-point.hh>
|
||||||
#include <psyqo/font.hh>
|
#include <psyqo/font.hh>
|
||||||
#include <psyqo/gpu.hh>
|
#include <psyqo/gpu.hh>
|
||||||
#include <psyqo/scene.hh>
|
#include <psyqo/scene.hh>
|
||||||
|
#include <psyqo/task.hh>
|
||||||
#include <psyqo/trigonometry.hh>
|
#include <psyqo/trigonometry.hh>
|
||||||
|
|
||||||
#include "camera.hh"
|
|
||||||
#include "gameobject.hh"
|
|
||||||
#include "renderer.hh"
|
#include "renderer.hh"
|
||||||
#include "splashpack.hh"
|
#include "scenemanager.hh"
|
||||||
|
#include "fileloader.hh"
|
||||||
|
|
||||||
extern uint8_t _binary_output_bin_start[];
|
#if defined(LOADER_CDROM)
|
||||||
|
#include "fileloader_cdrom.hh"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using namespace psyqo::fixed_point_literals;
|
|
||||||
using namespace psyqo::trig_literals;
|
|
||||||
|
|
||||||
class PSXSplash final : public psyqo::Application {
|
class PSXSplash final : public psyqo::Application {
|
||||||
void prepare() override;
|
void prepare() override;
|
||||||
void createScene() override;
|
void createScene() override;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
psyqo::Font<> m_font;
|
psyqo::Font<> m_font;
|
||||||
psyqo::AdvancedPad m_input;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class MainScene final : public psyqo::Scene {
|
class MainScene final : public psyqo::Scene {
|
||||||
void frame() override;
|
void frame() override;
|
||||||
void start(StartReason reason) override;
|
void start(StartReason reason) override;
|
||||||
|
|
||||||
psxsplash::Camera m_mainCamera;
|
|
||||||
psyqo::Angle camRotX, camRotY, camRotZ;
|
|
||||||
|
|
||||||
eastl::vector<psxsplash::GameObject*> m_objects;
|
|
||||||
psyqo::Trig<> m_trig;
|
|
||||||
uint32_t m_lastFrameCounter;
|
uint32_t m_lastFrameCounter;
|
||||||
|
|
||||||
static constexpr psyqo::FixedPoint<12> moveSpeed = 0.01_fp;
|
psxsplash::SceneManager m_sceneManager;
|
||||||
static constexpr psyqo::Angle rotSpeed = 0.01_pi;
|
|
||||||
|
// Task queue for async FileLoader init (CD-ROM reset + ISO parse).
|
||||||
|
// After init completes, loadScene() handles everything synchronously.
|
||||||
|
psyqo::TaskQueue m_initQueue;
|
||||||
|
bool m_ready = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
PSXSplash psxSplash;
|
PSXSplash app;
|
||||||
MainScene mainScene;
|
MainScene mainScene;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -58,88 +51,80 @@ void PSXSplash::prepare() {
|
|||||||
.set(psyqo::GPU::ColorMode::C15BITS)
|
.set(psyqo::GPU::ColorMode::C15BITS)
|
||||||
.set(psyqo::GPU::Interlace::PROGRESSIVE);
|
.set(psyqo::GPU::Interlace::PROGRESSIVE);
|
||||||
gpu().initialize(config);
|
gpu().initialize(config);
|
||||||
psxsplash::Renderer::init(gpu());
|
|
||||||
|
// Initialize the Renderer singleton
|
||||||
|
psxsplash::Renderer::Init(gpu());
|
||||||
|
|
||||||
|
// Clear screen
|
||||||
|
psyqo::Prim::FastFill ff(psyqo::Color{.r = 0, .g = 0, .b = 0});
|
||||||
|
ff.rect = psyqo::Rect{0, 0, 320, 240};
|
||||||
|
gpu().sendPrimitive(ff);
|
||||||
|
ff.rect = psyqo::Rect{0, 256, 320, 240};
|
||||||
|
gpu().sendPrimitive(ff);
|
||||||
|
gpu().pumpCallbacks();
|
||||||
|
|
||||||
|
// Let the active file-loader backend do any early setup.
|
||||||
|
// CDRom: CDRomDevice::prepare() must happen here.
|
||||||
|
psxsplash::FileLoader::Get().prepare();
|
||||||
|
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
// The CD-ROM backend needs a GPU pointer for LoadFileSync's spin loop.
|
||||||
|
static_cast<psxsplash::FileLoaderCDRom&>(
|
||||||
|
psxsplash::FileLoader::Get()).setGPU(&gpu());
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void PSXSplash::createScene() {
|
void PSXSplash::createScene() {
|
||||||
m_font.uploadSystemFont(gpu());
|
m_font.uploadSystemFont(gpu());
|
||||||
m_input.initialize();
|
psxsplash::SceneManager::SetFont(&m_font);
|
||||||
pushScene(&mainScene);
|
pushScene(&mainScene);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainScene::start(StartReason reason) {
|
void MainScene::start(StartReason reason) {
|
||||||
m_objects = psxsplash::LoadSplashpack(_binary_output_bin_start);
|
// Initialise the FileLoader backend, then load scene 0 through
|
||||||
psxsplash::Renderer::getInstance().setCamera(m_mainCamera);
|
// the same SceneManager::loadScene() path used for all transitions.
|
||||||
|
//
|
||||||
|
// For PCdrv the init task resolves synchronously so both steps
|
||||||
|
// execute in one go. For CD-ROM the init is async (drive reset +
|
||||||
|
// ISO9660 parse) and yields to the main loop until complete.
|
||||||
|
|
||||||
|
m_initQueue
|
||||||
|
.startWith(psxsplash::FileLoader::Get().scheduleInit())
|
||||||
|
.then([this](psyqo::TaskQueue::Task* task) {
|
||||||
|
m_sceneManager.loadScene(gpu(), 0, /*isFirstScene=*/true);
|
||||||
|
m_ready = true;
|
||||||
|
task->resolve();
|
||||||
|
})
|
||||||
|
.butCatch([](psyqo::TaskQueue*) {
|
||||||
|
// FileLoader init failed — nothing we can do on PS1.
|
||||||
|
})
|
||||||
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainScene::frame() {
|
void MainScene::frame() {
|
||||||
|
// Don't run the game loop while FileLoader init is still executing
|
||||||
|
// (only relevant for the async CD-ROM backend).
|
||||||
|
if (!m_ready) return;
|
||||||
|
|
||||||
uint32_t beginFrame = gpu().now();
|
uint32_t beginFrame = gpu().now();
|
||||||
auto currentFrameCounter = gpu().getFrameCount();
|
auto currentFrameCounter = gpu().getFrameCount();
|
||||||
auto frameDiff = currentFrameCounter - mainScene.m_lastFrameCounter;
|
auto deltaTime = currentFrameCounter - mainScene.m_lastFrameCounter;
|
||||||
if (frameDiff == 0) {
|
|
||||||
|
// Unlike the torus example, this DOES happen...
|
||||||
|
if (deltaTime == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainScene.m_lastFrameCounter = currentFrameCounter;
|
mainScene.m_lastFrameCounter = currentFrameCounter;
|
||||||
|
|
||||||
auto& input = psxSplash.m_input;
|
m_sceneManager.GameTick(gpu());
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Right)) {
|
#if defined(PSXSPLASH_FPSOVERLAY)
|
||||||
m_mainCamera.moveX((m_trig.cos(camRotY) * moveSpeed));
|
app.m_font.chainprintf(gpu(), {{.x = 2, .y = 2}}, {{.r = 0xff, .g = 0xff, .b = 0xff}}, "FPS: %i",
|
||||||
m_mainCamera.moveZ(-(m_trig.sin(camRotY) * moveSpeed));
|
gpu().getRefreshRate() / deltaTime);
|
||||||
}
|
#endif
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Left)) {
|
|
||||||
m_mainCamera.moveX(-(m_trig.cos(camRotY) * moveSpeed));
|
|
||||||
m_mainCamera.moveZ((m_trig.sin(camRotY) * moveSpeed));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Up)) {
|
|
||||||
m_mainCamera.moveX((m_trig.sin(camRotY) * m_trig.cos(camRotX)) * moveSpeed);
|
|
||||||
m_mainCamera.moveY(-(m_trig.sin(camRotX) * moveSpeed));
|
|
||||||
m_mainCamera.moveZ((m_trig.cos(camRotY) * m_trig.cos(camRotX)) * moveSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Down)) {
|
|
||||||
m_mainCamera.moveX(-((m_trig.sin(camRotY) * m_trig.cos(camRotX)) * moveSpeed));
|
|
||||||
m_mainCamera.moveY((m_trig.sin(camRotX) * moveSpeed));
|
|
||||||
m_mainCamera.moveZ(-((m_trig.cos(camRotY) * m_trig.cos(camRotX)) * moveSpeed));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::R1)) {
|
|
||||||
m_mainCamera.moveY(-moveSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::L1)) {
|
|
||||||
m_mainCamera.moveY(moveSpeed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Cross)) {
|
|
||||||
camRotX -= rotSpeed;
|
|
||||||
m_mainCamera.setRotation(camRotX, camRotY, camRotZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Triangle)) {
|
|
||||||
camRotX += rotSpeed;
|
|
||||||
m_mainCamera.setRotation(camRotX, camRotY, camRotZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Circle)) {
|
|
||||||
camRotY += rotSpeed;
|
|
||||||
m_mainCamera.setRotation(camRotX, camRotY, camRotZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Square)) {
|
|
||||||
camRotY -= rotSpeed;
|
|
||||||
m_mainCamera.setRotation(camRotX, camRotY, camRotZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
psxsplash::Renderer::getInstance().render(m_objects);
|
|
||||||
|
|
||||||
psxSplash.m_font.chainprintf(gpu(), {{.x = 2, .y = 2}}, {{.r = 0xff, .g = 0xff, .b = 0xff}}, "FPS: %i",
|
|
||||||
gpu().getRefreshRate() / frameDiff);
|
|
||||||
gpu().pumpCallbacks();
|
gpu().pumpCallbacks();
|
||||||
uint32_t endFrame = gpu().now();
|
|
||||||
uint32_t spent = endFrame - beginFrame;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() { return psxSplash.run(); }
|
int main() { return app.run(); }
|
||||||
96
src/memoverlay.cpp
Normal file
96
src/memoverlay.cpp
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
|
||||||
|
#include "memoverlay.hh"
|
||||||
|
|
||||||
|
#include <psyqo/alloc.h>
|
||||||
|
#include <psyqo/primitives/rectangles.hh>
|
||||||
|
|
||||||
|
#include "vram_config.h"
|
||||||
|
|
||||||
|
// Linker symbols
|
||||||
|
extern "C" {
|
||||||
|
extern char __heap_start;
|
||||||
|
extern char __stack_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
void MemOverlay::init(psyqo::Font<>* font) {
|
||||||
|
m_font = font;
|
||||||
|
m_heapBase = reinterpret_cast<uintptr_t>(&__heap_start);
|
||||||
|
m_stackTop = reinterpret_cast<uintptr_t>(&__stack_start);
|
||||||
|
m_totalAvail = (uint32_t)(m_stackTop - m_heapBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemOverlay::renderOT(psyqo::OrderingTable<Renderer::ORDERING_TABLE_SIZE>& ot,
|
||||||
|
psyqo::BumpAllocator<Renderer::BUMP_ALLOCATOR_SIZE>& balloc) {
|
||||||
|
if (!m_font || m_totalAvail == 0) return;
|
||||||
|
|
||||||
|
// Measure current heap usage
|
||||||
|
void* heapEnd = psyqo_heap_end();
|
||||||
|
uint32_t heapUsed = 0;
|
||||||
|
if (heapEnd != nullptr) {
|
||||||
|
heapUsed = (uint32_t)(reinterpret_cast<uintptr_t>(heapEnd) - m_heapBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lastPct = (heapUsed * 100) / m_totalAvail;
|
||||||
|
if (m_lastPct > 100) m_lastPct = 100;
|
||||||
|
m_lastUsedKB = heapUsed / 1024;
|
||||||
|
m_lastTotalKB = m_totalAvail / 1024;
|
||||||
|
|
||||||
|
// Bar dimensions - top-right corner
|
||||||
|
static constexpr int16_t BAR_W = 80;
|
||||||
|
static constexpr int16_t BAR_H = 6;
|
||||||
|
static constexpr int16_t BAR_X = VRAM_RES_WIDTH - BAR_W - 4;
|
||||||
|
static constexpr int16_t BAR_Y = 4;
|
||||||
|
|
||||||
|
// Need room for two rectangles
|
||||||
|
size_t needed = sizeof(psyqo::Fragments::SimpleFragment<psyqo::Prim::Rectangle>) * 2 + 32;
|
||||||
|
if (balloc.remaining() < needed) return;
|
||||||
|
|
||||||
|
// Fill bar first (OT is LIFO, so first insert renders last / on top)
|
||||||
|
int16_t fillW = (int16_t)((uint32_t)BAR_W * m_lastPct / 100);
|
||||||
|
if (fillW > 0) {
|
||||||
|
uint8_t r, g;
|
||||||
|
if (m_lastPct < 50) {
|
||||||
|
r = (uint8_t)(m_lastPct * 5);
|
||||||
|
g = 200;
|
||||||
|
} else {
|
||||||
|
r = 250;
|
||||||
|
g = (uint8_t)((100 - m_lastPct) * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& fillFrag = balloc.allocateFragment<psyqo::Prim::Rectangle>();
|
||||||
|
fillFrag.primitive.setColor(psyqo::Color{.r = r, .g = g, .b = 20});
|
||||||
|
fillFrag.primitive.position = {.x = BAR_X, .y = BAR_Y};
|
||||||
|
fillFrag.primitive.size = {.x = fillW, .y = BAR_H};
|
||||||
|
fillFrag.primitive.setOpaque();
|
||||||
|
ot.insert(fillFrag, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background bar (inserted second, so renders first / behind fill)
|
||||||
|
auto& bgFrag = balloc.allocateFragment<psyqo::Prim::Rectangle>();
|
||||||
|
bgFrag.primitive.setColor(psyqo::Color{.r = 40, .g = 40, .b = 40});
|
||||||
|
bgFrag.primitive.position = {.x = BAR_X, .y = BAR_Y};
|
||||||
|
bgFrag.primitive.size = {.x = BAR_W, .y = BAR_H};
|
||||||
|
bgFrag.primitive.setOpaque();
|
||||||
|
ot.insert(bgFrag, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MemOverlay::renderText(psyqo::GPU& gpu) {
|
||||||
|
if (!m_font || m_totalAvail == 0) return;
|
||||||
|
|
||||||
|
static constexpr int16_t BAR_H = 6;
|
||||||
|
static constexpr int16_t BAR_X = VRAM_RES_WIDTH - 80 - 4;
|
||||||
|
static constexpr int16_t BAR_Y = 4;
|
||||||
|
static constexpr int16_t TEXT_Y = BAR_Y + BAR_H + 2;
|
||||||
|
|
||||||
|
m_font->chainprintf(gpu,
|
||||||
|
{{.x = BAR_X - 20, .y = TEXT_Y}},
|
||||||
|
{{.r = 0xcc, .g = 0xcc, .b = 0xcc}},
|
||||||
|
"%luK/%luK", m_lastUsedKB, m_lastTotalKB);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
|
|
||||||
|
#endif // PSXSPLASH_MEMOVERLAY
|
||||||
47
src/memoverlay.hh
Normal file
47
src/memoverlay.hh
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Runtime memory overlay - shows heap/RAM usage as a progress bar + text.
|
||||||
|
// Compiled only when PSXSPLASH_MEMOVERLAY is defined (set from SplashEdit build flag).
|
||||||
|
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
|
||||||
|
#include <psyqo/font.hh>
|
||||||
|
#include <psyqo/gpu.hh>
|
||||||
|
#include <psyqo/bump-allocator.hh>
|
||||||
|
#include <psyqo/ordering-table.hh>
|
||||||
|
|
||||||
|
#include "renderer.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
class MemOverlay {
|
||||||
|
public:
|
||||||
|
/// Call once at startup to cache linker symbol addresses.
|
||||||
|
void init(psyqo::Font<>* font);
|
||||||
|
|
||||||
|
/// Phase 1: Insert progress bar rectangles into the OT.
|
||||||
|
/// Call BEFORE gpu.chain(ot).
|
||||||
|
void renderOT(psyqo::OrderingTable<Renderer::ORDERING_TABLE_SIZE>& ot,
|
||||||
|
psyqo::BumpAllocator<Renderer::BUMP_ALLOCATOR_SIZE>& balloc);
|
||||||
|
|
||||||
|
/// Phase 2: Emit text via chainprintf.
|
||||||
|
/// Call AFTER gpu.chain(ot).
|
||||||
|
void renderText(psyqo::GPU& gpu);
|
||||||
|
|
||||||
|
private:
|
||||||
|
psyqo::Font<>* m_font = nullptr;
|
||||||
|
|
||||||
|
// Cached addresses from linker symbols
|
||||||
|
uintptr_t m_heapBase = 0;
|
||||||
|
uintptr_t m_stackTop = 0;
|
||||||
|
uint32_t m_totalAvail = 0;
|
||||||
|
|
||||||
|
// Cached each frame for text phase
|
||||||
|
uint32_t m_lastUsedKB = 0;
|
||||||
|
uint32_t m_lastTotalKB = 0;
|
||||||
|
uint32_t m_lastPct = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
|
|
||||||
|
#endif // PSXSPLASH_MEMOVERLAY
|
||||||
22
src/mesh.hh
22
src/mesh.hh
@@ -1,17 +1,33 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "psyqo/gte-registers.hh"
|
#include <psyqo/gte-registers.hh>
|
||||||
#include "psyqo/primitives/common.hh"
|
#include <psyqo/primitives/common.hh>
|
||||||
|
|
||||||
namespace psxsplash {
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// Sentinel value for untextured (vertex-color-only) triangles
|
||||||
|
static constexpr uint16_t UNTEXTURED_TPAGE = 0xFFFF;
|
||||||
|
|
||||||
class Tri final {
|
class Tri final {
|
||||||
public:
|
public:
|
||||||
psyqo::GTE::PackedVec3 v0, v1, v2;
|
psyqo::GTE::PackedVec3 v0, v1, v2;
|
||||||
psyqo::GTE::PackedVec3 normal;
|
psyqo::GTE::PackedVec3 normal;
|
||||||
|
|
||||||
|
psyqo::Color colorA, colorB, colorC;
|
||||||
|
|
||||||
psyqo::PrimPieces::UVCoords uvA, uvB;
|
psyqo::PrimPieces::UVCoords uvA, uvB;
|
||||||
psyqo::PrimPieces::UVCoordsPadded uvC;
|
psyqo::PrimPieces::UVCoordsPadded uvC;
|
||||||
psyqo::Color colorA, colorB, colorC;
|
|
||||||
|
psyqo::PrimPieces::TPageAttr tpage;
|
||||||
|
uint16_t clutX;
|
||||||
|
uint16_t clutY;
|
||||||
|
uint16_t padding;
|
||||||
|
|
||||||
|
/// Returns true if this triangle has no texture (vertex-color only).
|
||||||
|
bool isUntextured() const {
|
||||||
|
return *reinterpret_cast<const uint16_t*>(&tpage) == UNTEXTURED_TPAGE;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
static_assert(sizeof(Tri) == 52, "Tri is not 52 bytes");
|
||||||
|
|
||||||
} // namespace psxsplash
|
} // namespace psxsplash
|
||||||
|
|||||||
291
src/navregion.cpp
Normal file
291
src/navregion.cpp
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
#include "navregion.hh"
|
||||||
|
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* navregion.cpp - Convex Region Navigation System
|
||||||
|
*
|
||||||
|
* All math is 20.12 fixed-point. Zero floats.
|
||||||
|
*
|
||||||
|
* Key operations:
|
||||||
|
* - resolvePosition: O(1) typical (check current + neighbors via portals)
|
||||||
|
* - pointInRegion: O(n) per polygon vertices (convex cross test)
|
||||||
|
* - getFloorY: O(1) plane equation evaluation
|
||||||
|
* - findRegion: O(R) brute force, used only at init
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Fixed-point helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static constexpr int FRAC_BITS = 12;
|
||||||
|
static constexpr int32_t FP_ONE = 1 << FRAC_BITS;
|
||||||
|
|
||||||
|
static inline int32_t fpmul(int32_t a, int32_t b) {
|
||||||
|
return (int32_t)(((int64_t)a * b) >> FRAC_BITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int32_t fpdiv(int32_t a, int32_t b) {
|
||||||
|
if (b == 0) return 0;
|
||||||
|
int32_t q = a / b;
|
||||||
|
int32_t r = a - q * b;
|
||||||
|
return q * FP_ONE + (r << FRAC_BITS) / b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Initialization
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const uint8_t* NavRegionSystem::initializeFromData(const uint8_t* data) {
|
||||||
|
const auto* hdr = reinterpret_cast<const NavDataHeader*>(data);
|
||||||
|
m_header = *hdr;
|
||||||
|
data += sizeof(NavDataHeader);
|
||||||
|
|
||||||
|
m_regions = reinterpret_cast<const NavRegion*>(data);
|
||||||
|
data += m_header.regionCount * sizeof(NavRegion);
|
||||||
|
|
||||||
|
m_portals = reinterpret_cast<const NavPortal*>(data);
|
||||||
|
data += m_header.portalCount * sizeof(NavPortal);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Point-in-convex-polygon (XZ plane)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool NavRegionSystem::pointInConvexPoly(int32_t px, int32_t pz,
|
||||||
|
const int32_t* vertsX, const int32_t* vertsZ,
|
||||||
|
int vertCount) {
|
||||||
|
if (vertCount < 3) return false;
|
||||||
|
|
||||||
|
// For CCW winding, all cross products must be >= 0.
|
||||||
|
// cross = (bx - ax) * (pz - az) - (bz - az) * (px - ax)
|
||||||
|
for (int i = 0; i < vertCount; i++) {
|
||||||
|
int next = (i + 1) % vertCount;
|
||||||
|
int32_t ax = vertsX[i], az = vertsZ[i];
|
||||||
|
int32_t bx = vertsX[next], bz = vertsZ[next];
|
||||||
|
|
||||||
|
// Edge direction
|
||||||
|
int32_t edgeX = bx - ax;
|
||||||
|
int32_t edgeZ = bz - az;
|
||||||
|
// Point relative to edge start
|
||||||
|
int32_t relX = px - ax;
|
||||||
|
int32_t relZ = pz - az;
|
||||||
|
|
||||||
|
// Cross product (64-bit to prevent overflow)
|
||||||
|
int64_t cross = (int64_t)edgeX * relZ - (int64_t)edgeZ * relX;
|
||||||
|
if (cross < 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Closest point on segment (XZ only)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void NavRegionSystem::closestPointOnSegment(int32_t px, int32_t pz,
|
||||||
|
int32_t ax, int32_t az,
|
||||||
|
int32_t bx, int32_t bz,
|
||||||
|
int32_t& outX, int32_t& outZ) {
|
||||||
|
int32_t abx = bx - ax;
|
||||||
|
int32_t abz = bz - az;
|
||||||
|
int32_t lenSq = fpmul(abx, abx) + fpmul(abz, abz);
|
||||||
|
if (lenSq == 0) {
|
||||||
|
outX = ax; outZ = az;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t dot = fpmul(px - ax, abx) + fpmul(pz - az, abz);
|
||||||
|
// t = dot / lenSq, clamped to [0, 1]
|
||||||
|
int32_t t;
|
||||||
|
if (dot <= 0) {
|
||||||
|
t = 0;
|
||||||
|
} else if (dot >= lenSq) {
|
||||||
|
t = FP_ONE;
|
||||||
|
} else {
|
||||||
|
t = fpdiv(dot, lenSq);
|
||||||
|
}
|
||||||
|
|
||||||
|
outX = ax + fpmul(t, abx);
|
||||||
|
outZ = az + fpmul(t, abz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Get floor Y at position (plane equation)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
int32_t NavRegionSystem::getFloorY(int32_t x, int32_t z, uint16_t regionIndex) const {
|
||||||
|
if (regionIndex >= m_header.regionCount) return 0;
|
||||||
|
const auto& reg = m_regions[regionIndex];
|
||||||
|
|
||||||
|
// Y = planeA * X + planeB * Z + planeD
|
||||||
|
// (all in 20.12, products need 64-bit intermediate)
|
||||||
|
return fpmul(reg.planeA, x) + fpmul(reg.planeB, z) + reg.planeD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Point in region test
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool NavRegionSystem::pointInRegion(int32_t x, int32_t z, uint16_t regionIndex) const {
|
||||||
|
if (regionIndex >= m_header.regionCount) return false;
|
||||||
|
const auto& reg = m_regions[regionIndex];
|
||||||
|
return pointInConvexPoly(x, z, reg.vertsX, reg.vertsZ, reg.vertCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Find region (brute force, for initialization)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
uint16_t NavRegionSystem::findRegion(int32_t x, int32_t z) const {
|
||||||
|
// When multiple regions overlap at the same XZ position (e.g., floor and
|
||||||
|
// elevated step), prefer the highest physical surface. In PSX Y-down space,
|
||||||
|
// highest surface = smallest (most negative) floor Y value.
|
||||||
|
uint16_t best = NAV_NO_REGION;
|
||||||
|
int32_t bestY = 0x7FFFFFFF;
|
||||||
|
for (uint16_t i = 0; i < m_header.regionCount; i++) {
|
||||||
|
if (pointInRegion(x, z, i)) {
|
||||||
|
int32_t fy = getFloorY(x, z, i);
|
||||||
|
if (fy < bestY) {
|
||||||
|
bestY = fy;
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Clamp position to region boundary
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void NavRegionSystem::clampToRegion(int32_t& x, int32_t& z, uint16_t regionIndex) const {
|
||||||
|
if (regionIndex >= m_header.regionCount) return;
|
||||||
|
const auto& reg = m_regions[regionIndex];
|
||||||
|
|
||||||
|
if (pointInConvexPoly(x, z, reg.vertsX, reg.vertsZ, reg.vertCount))
|
||||||
|
return; // Already inside
|
||||||
|
|
||||||
|
// Find closest point on any edge of the polygon
|
||||||
|
int32_t bestX = x, bestZ = z;
|
||||||
|
int64_t bestDistSq = 0x7FFFFFFFFFFFFFFFLL;
|
||||||
|
|
||||||
|
for (int i = 0; i < reg.vertCount; i++) {
|
||||||
|
int next = (i + 1) % reg.vertCount;
|
||||||
|
int32_t cx, cz;
|
||||||
|
closestPointOnSegment(x, z,
|
||||||
|
reg.vertsX[i], reg.vertsZ[i],
|
||||||
|
reg.vertsX[next], reg.vertsZ[next],
|
||||||
|
cx, cz);
|
||||||
|
|
||||||
|
int64_t dx = (int64_t)(x - cx);
|
||||||
|
int64_t dz = (int64_t)(z - cz);
|
||||||
|
int64_t distSq = dx * dx + dz * dz;
|
||||||
|
|
||||||
|
if (distSq < bestDistSq) {
|
||||||
|
bestDistSq = distSq;
|
||||||
|
bestX = cx;
|
||||||
|
bestZ = cz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x = bestX;
|
||||||
|
z = bestZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Resolve position (main per-frame call)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
int32_t NavRegionSystem::resolvePosition(int32_t& newX, int32_t& newZ,
|
||||||
|
uint16_t& currentRegion) const {
|
||||||
|
if (!isLoaded() || m_header.regionCount == 0) return 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If no valid region, find one
|
||||||
|
if (currentRegion == NAV_NO_REGION || currentRegion >= m_header.regionCount) {
|
||||||
|
currentRegion = findRegion(newX, newZ);
|
||||||
|
|
||||||
|
if (currentRegion == NAV_NO_REGION) return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if still in current region
|
||||||
|
if (pointInRegion(newX, newZ, currentRegion)) {
|
||||||
|
int32_t fy = getFloorY(newX, newZ, currentRegion);
|
||||||
|
|
||||||
|
// Check if a portal neighbor has a higher floor at this position.
|
||||||
|
// This handles overlapping regions (e.g., floor and elevated step).
|
||||||
|
// When the player walks onto the step, the step region (portal neighbor)
|
||||||
|
// has a higher floor (smaller Y in PSX Y-down) and should take priority.
|
||||||
|
const auto& reg = m_regions[currentRegion];
|
||||||
|
for (int i = 0; i < reg.portalCount; i++) {
|
||||||
|
uint16_t portalIdx = reg.portalStart + i;
|
||||||
|
if (portalIdx >= m_header.portalCount) break;
|
||||||
|
uint16_t neighbor = m_portals[portalIdx].neighborRegion;
|
||||||
|
if (neighbor >= m_header.regionCount) continue;
|
||||||
|
if (pointInRegion(newX, newZ, neighbor)) {
|
||||||
|
int32_t nfy = getFloorY(newX, newZ, neighbor);
|
||||||
|
if (nfy < fy) { // Higher physical surface (Y-down: smaller = higher)
|
||||||
|
currentRegion = neighbor;
|
||||||
|
fy = nfy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Check portal neighbors
|
||||||
|
const auto& reg = m_regions[currentRegion];
|
||||||
|
for (int i = 0; i < reg.portalCount; i++) {
|
||||||
|
uint16_t portalIdx = reg.portalStart + i;
|
||||||
|
if (portalIdx >= m_header.portalCount) break;
|
||||||
|
|
||||||
|
const auto& portal = m_portals[portalIdx];
|
||||||
|
uint16_t neighbor = portal.neighborRegion;
|
||||||
|
|
||||||
|
if (neighbor < m_header.regionCount && pointInRegion(newX, newZ, neighbor)) {
|
||||||
|
currentRegion = neighbor;
|
||||||
|
return getFloorY(newX, newZ, neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in current region or any neighbor — try broader search
|
||||||
|
// This handles jumping/falling to non-adjacent regions (e.g., landing on a platform)
|
||||||
|
{
|
||||||
|
uint16_t found = findRegion(newX, newZ);
|
||||||
|
if (found != NAV_NO_REGION) {
|
||||||
|
currentRegion = found;
|
||||||
|
return getFloorY(newX, newZ, found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truly off all regions — clamp to current region boundary
|
||||||
|
clampToRegion(newX, newZ, currentRegion);
|
||||||
|
return getFloorY(newX, newZ, currentRegion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Pathfinding stub
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool NavRegionSystem::findPath(uint16_t startRegion, uint16_t endRegion,
|
||||||
|
NavPath& path) const {
|
||||||
|
// STUB: Returns false until NPC pathfinding is implemented.
|
||||||
|
path.stepCount = 0;
|
||||||
|
(void)startRegion;
|
||||||
|
(void)endRegion;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
171
src/navregion.hh
Normal file
171
src/navregion.hh
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* navregion.hh - Convex Region Navigation System
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* - Walkable surface decomposed into convex polygonal regions (XZ plane).
|
||||||
|
* - Adjacent regions share portal edges.
|
||||||
|
* - Player has a single current region index.
|
||||||
|
* - Movement: point-in-convex-polygon test → portal crossing → neighbor update.
|
||||||
|
* - Floor Y: project XZ onto region's floor plane.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Constants
|
||||||
|
// ============================================================================
|
||||||
|
static constexpr int NAV_MAX_VERTS_PER_REGION = 8; // Max polygon verts
|
||||||
|
static constexpr int NAV_MAX_NEIGHBORS = 8; // Max portal edges per region
|
||||||
|
static constexpr int NAV_MAX_PATH_STEPS = 32; // Max A* path length
|
||||||
|
static constexpr uint16_t NAV_NO_REGION = 0xFFFF; // Sentinel: no region
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Surface type for navigation regions
|
||||||
|
// ============================================================================
|
||||||
|
enum NavSurfaceType : uint8_t {
|
||||||
|
NAV_SURFACE_FLAT = 0,
|
||||||
|
NAV_SURFACE_RAMP = 1,
|
||||||
|
NAV_SURFACE_STAIRS = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Portal edge — shared edge between two adjacent regions
|
||||||
|
// ============================================================================
|
||||||
|
struct NavPortal {
|
||||||
|
int32_t ax, az; // Portal edge start (20.12 XZ)
|
||||||
|
int32_t bx, bz; // Portal edge end (20.12 XZ)
|
||||||
|
uint16_t neighborRegion; // Index of the region on the other side
|
||||||
|
int16_t heightDelta; // Vertical step in 4.12 (stairs, ledges)
|
||||||
|
};
|
||||||
|
static_assert(sizeof(NavPortal) == 20, "NavPortal must be 20 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Nav Region — convex polygon on the XZ plane with floor info
|
||||||
|
// ============================================================================
|
||||||
|
struct NavRegion {
|
||||||
|
// Convex polygon vertices (XZ, 20.12 fixed-point)
|
||||||
|
// Stored in CCW winding order
|
||||||
|
int32_t vertsX[NAV_MAX_VERTS_PER_REGION]; // 32 bytes
|
||||||
|
int32_t vertsZ[NAV_MAX_VERTS_PER_REGION]; // 32 bytes
|
||||||
|
|
||||||
|
// Floor plane: Y = planeA * X + planeB * Z + planeD (all 20.12)
|
||||||
|
// For flat floors: planeA = planeB = 0, planeD = floorY
|
||||||
|
int32_t planeA, planeB, planeD; // 12 bytes
|
||||||
|
|
||||||
|
// Portal neighbors
|
||||||
|
uint16_t portalStart; // Index into portal array 2 bytes
|
||||||
|
uint8_t portalCount; // Number of portals 1 byte
|
||||||
|
uint8_t vertCount; // Number of polygon verts 1 byte
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
NavSurfaceType surfaceType; // 1 byte
|
||||||
|
uint8_t roomIndex; // Interior room (0xFF = exterior) 1 byte
|
||||||
|
uint8_t pad[2]; // Alignment 2 bytes
|
||||||
|
// Total: 32 + 32 + 12 + 4 + 4 = 84 bytes
|
||||||
|
};
|
||||||
|
static_assert(sizeof(NavRegion) == 84, "NavRegion must be 84 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Nav data header
|
||||||
|
// ============================================================================
|
||||||
|
struct NavDataHeader {
|
||||||
|
uint16_t regionCount;
|
||||||
|
uint16_t portalCount;
|
||||||
|
uint16_t startRegion; // Region the player spawns in
|
||||||
|
uint16_t pad;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(NavDataHeader) == 8, "NavDataHeader must be 8 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Path result for A* (used by NPC pathfinding)
|
||||||
|
// ============================================================================
|
||||||
|
struct NavPath {
|
||||||
|
uint16_t regions[NAV_MAX_PATH_STEPS];
|
||||||
|
int stepCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NavRegionSystem — manages navigation at runtime
|
||||||
|
// ============================================================================
|
||||||
|
class NavRegionSystem {
|
||||||
|
public:
|
||||||
|
NavRegionSystem() = default;
|
||||||
|
|
||||||
|
/// Initialize from splashpack data. Returns pointer past the data.
|
||||||
|
const uint8_t* initializeFromData(const uint8_t* data);
|
||||||
|
|
||||||
|
/// Is nav data loaded?
|
||||||
|
bool isLoaded() const { return m_regions != nullptr; }
|
||||||
|
|
||||||
|
void relocate(intptr_t delta) {
|
||||||
|
if (m_regions) m_regions = reinterpret_cast<const NavRegion*>(reinterpret_cast<intptr_t>(m_regions) + delta);
|
||||||
|
if (m_portals) m_portals = reinterpret_cast<const NavPortal*>(reinterpret_cast<intptr_t>(m_portals) + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of regions
|
||||||
|
uint16_t getRegionCount() const { return m_header.regionCount; }
|
||||||
|
|
||||||
|
/// Get the start region
|
||||||
|
uint16_t getStartRegion() const { return m_header.startRegion; }
|
||||||
|
|
||||||
|
/// Get the room index for a given nav region (0xFF = exterior/unknown)
|
||||||
|
uint8_t getRoomIndex(uint16_t regionIndex) const {
|
||||||
|
if (m_regions == nullptr || regionIndex >= m_header.regionCount) return 0xFF;
|
||||||
|
return m_regions[regionIndex].roomIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Player movement - called every frame
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Given a new XZ position and the player's current region,
|
||||||
|
/// determine the correct region and return the floor Y.
|
||||||
|
/// Updates currentRegion in-place.
|
||||||
|
/// newX/newZ are clamped to stay within the region boundary.
|
||||||
|
/// Returns the Y position for the player's feet.
|
||||||
|
int32_t resolvePosition(int32_t& newX, int32_t& newZ,
|
||||||
|
uint16_t& currentRegion) const;
|
||||||
|
|
||||||
|
/// Test if a point (XZ) is inside a specific region.
|
||||||
|
bool pointInRegion(int32_t x, int32_t z, uint16_t regionIndex) const;
|
||||||
|
|
||||||
|
/// Compute floor Y at a given XZ within a region using the floor plane.
|
||||||
|
int32_t getFloorY(int32_t x, int32_t z, uint16_t regionIndex) const;
|
||||||
|
|
||||||
|
/// Find which region contains a point (brute-force, for initialization).
|
||||||
|
uint16_t findRegion(int32_t x, int32_t z) const;
|
||||||
|
|
||||||
|
/// Clamp an XZ position to stay within a region's polygon boundary.
|
||||||
|
/// Returns the clamped position.
|
||||||
|
void clampToRegion(int32_t& x, int32_t& z, uint16_t regionIndex) const;
|
||||||
|
|
||||||
|
// TODO: Implement this
|
||||||
|
bool findPath(uint16_t startRegion, uint16_t endRegion,
|
||||||
|
NavPath& path) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
NavDataHeader m_header = {};
|
||||||
|
const NavRegion* m_regions = nullptr;
|
||||||
|
const NavPortal* m_portals = nullptr;
|
||||||
|
|
||||||
|
/// Point-in-convex-polygon test (XZ plane).
|
||||||
|
/// Uses cross-product sign consistency (all edges same winding).
|
||||||
|
static bool pointInConvexPoly(int32_t px, int32_t pz,
|
||||||
|
const int32_t* vertsX, const int32_t* vertsZ,
|
||||||
|
int vertCount);
|
||||||
|
|
||||||
|
/// Closest point on a line segment to a point (XZ only)
|
||||||
|
static void closestPointOnSegment(int32_t px, int32_t pz,
|
||||||
|
int32_t ax, int32_t az,
|
||||||
|
int32_t bx, int32_t bz,
|
||||||
|
int32_t& outX, int32_t& outZ);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
182
src/pcdrv_handler.hh
Normal file
182
src/pcdrv_handler.hh
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/kernel.hh>
|
||||||
|
#include "common/hardware/pcsxhw.h"
|
||||||
|
#include "common/kernel/pcdrv.h"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// SIO1 hardware registers (UART serial port at 0x1F801050)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
#define SIO1_DATA (*(volatile uint8_t*)0x1F801050)
|
||||||
|
#define SIO1_STAT (*(volatile uint32_t*)0x1F801054)
|
||||||
|
#define SIO1_MODE (*(volatile uint16_t*)0x1F801058)
|
||||||
|
#define SIO1_CTRL (*(volatile uint16_t*)0x1F80105A)
|
||||||
|
#define SIO1_BAUD (*(volatile uint16_t*)0x1F80105E)
|
||||||
|
|
||||||
|
#define SIO1_TX_RDY (1 << 0)
|
||||||
|
#define SIO1_RX_RDY (1 << 1)
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Low-level SIO1 I/O - blocking, tight polling loops
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static inline void sio_putc(uint8_t byte) {
|
||||||
|
while (!(SIO1_STAT & SIO1_TX_RDY)) {}
|
||||||
|
SIO1_DATA = byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint8_t sio_getc() {
|
||||||
|
while (!(SIO1_STAT & SIO1_RX_RDY)) {}
|
||||||
|
return SIO1_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void sio_write32(uint32_t val) {
|
||||||
|
sio_putc((uint8_t)(val));
|
||||||
|
sio_putc((uint8_t)(val >> 8));
|
||||||
|
sio_putc((uint8_t)(val >> 16));
|
||||||
|
sio_putc((uint8_t)(val >> 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t sio_read32() {
|
||||||
|
uint32_t v = (uint32_t)sio_getc();
|
||||||
|
v |= (uint32_t)sio_getc() << 8;
|
||||||
|
v |= (uint32_t)sio_getc() << 16;
|
||||||
|
v |= (uint32_t)sio_getc() << 24;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool sio_check_okay() {
|
||||||
|
return sio_getc() == 'O' && sio_getc() == 'K'
|
||||||
|
&& sio_getc() == 'A' && sio_getc() == 'Y';
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void sio_pcdrv_escape(uint32_t funcCode) {
|
||||||
|
sio_putc(0x00);
|
||||||
|
sio_putc('p');
|
||||||
|
sio_write32(funcCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int sio_pcdrv_init() {
|
||||||
|
sio_pcdrv_escape(0x101);
|
||||||
|
if (sio_check_okay()) {
|
||||||
|
sio_getc(); // trailing 0x00
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sio_pcdrv_open(const char* name, int flags) {
|
||||||
|
sio_pcdrv_escape(0x103);
|
||||||
|
if (!sio_check_okay()) return -1;
|
||||||
|
const char* p = name;
|
||||||
|
while (*p) sio_putc((uint8_t)*p++);
|
||||||
|
sio_putc(0x00);
|
||||||
|
sio_write32((uint32_t)flags);
|
||||||
|
if (sio_check_okay()) {
|
||||||
|
return (int)sio_read32(); // handle
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sio_pcdrv_close(int fd) {
|
||||||
|
sio_pcdrv_escape(0x104);
|
||||||
|
if (!sio_check_okay()) return -1;
|
||||||
|
sio_write32((uint32_t)fd);
|
||||||
|
sio_write32(0); // unused
|
||||||
|
sio_write32(0); // unused
|
||||||
|
if (sio_check_okay()) {
|
||||||
|
sio_read32(); // handle echo
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sio_pcdrv_read(int fd, void* buf, int len) {
|
||||||
|
sio_pcdrv_escape(0x105);
|
||||||
|
if (!sio_check_okay()) return -1;
|
||||||
|
sio_write32((uint32_t)fd);
|
||||||
|
sio_write32((uint32_t)len);
|
||||||
|
sio_write32((uint32_t)(uintptr_t)buf); // memaddr for host debug
|
||||||
|
if (sio_check_okay()) {
|
||||||
|
uint32_t dataLen = sio_read32();
|
||||||
|
sio_read32(); // checksum (not verified)
|
||||||
|
uint8_t* dst = (uint8_t*)buf;
|
||||||
|
for (uint32_t i = 0; i < dataLen; i++) {
|
||||||
|
dst[i] = sio_getc();
|
||||||
|
}
|
||||||
|
return (int)dataLen;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sio_pcdrv_seek(int fd, int offset, int whence) {
|
||||||
|
sio_pcdrv_escape(0x107);
|
||||||
|
if (!sio_check_okay()) return -1;
|
||||||
|
sio_write32((uint32_t)fd);
|
||||||
|
sio_write32((uint32_t)offset);
|
||||||
|
sio_write32((uint32_t)whence);
|
||||||
|
if (sio_check_okay()) {
|
||||||
|
return (int)sio_read32(); // new position
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Public PCDRV API - runtime dispatch between emulator and real hardware
|
||||||
|
// Use these instead of pcdrv.h functions (PCopen, PCread, etc.)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static int pcdrv_init() {
|
||||||
|
if (pcsx_present()) return PCinit();
|
||||||
|
return sio_pcdrv_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pcdrv_open(const char* name, int flags, int perms) {
|
||||||
|
if (pcsx_present()) return PCopen(name, flags, perms);
|
||||||
|
return sio_pcdrv_open(name, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pcdrv_close(int fd) {
|
||||||
|
if (pcsx_present()) return PCclose(fd);
|
||||||
|
return sio_pcdrv_close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pcdrv_read(int fd, void* buf, int len) {
|
||||||
|
if (pcsx_present()) return PCread(fd, buf, len);
|
||||||
|
return sio_pcdrv_read(fd, buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int pcdrv_seek(int fd, int offset, int whence) {
|
||||||
|
if (pcsx_present()) return PClseek(fd, offset, whence);
|
||||||
|
return sio_pcdrv_seek(fd, offset, whence);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// SIO1 initialization - 115200 baud, 8N1
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static void sio1Init() {
|
||||||
|
SIO1_CTRL = 0; // reset
|
||||||
|
SIO1_MODE = 0x004e; // MUL16, 8 data, no parity, 1 stop
|
||||||
|
SIO1_BAUD = (uint16_t)(2073600 / 115200); // = 18
|
||||||
|
SIO1_CTRL = 0x0025; // TX enable, RX enable, RTS assert
|
||||||
|
for (int i = 0; i < 100; i++) { __asm__ volatile("" ::: "memory"); } // settle delay
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void pcdrv_sio1_init() {
|
||||||
|
if (pcsx_present()) return; // emulator handles PCDRV natively
|
||||||
|
|
||||||
|
sio1Init();
|
||||||
|
|
||||||
|
// TODO: printf redirect (redirectPrintfToSIO1) disabled for now.
|
||||||
|
// Printf redirect patches A0 handler machine code at 0xa8/0xb4
|
||||||
|
// and may cause instability - needs further testing.
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
31
src/profiler.cpp
Normal file
31
src/profiler.cpp
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#include "profiler.hh"
|
||||||
|
|
||||||
|
#ifdef PSXSPLASH_PROFILER
|
||||||
|
|
||||||
|
using namespace psxsplash::debug;
|
||||||
|
|
||||||
|
// Writes address+name to the PCSX-Redux debugger variable registry.
|
||||||
|
static void pcsxRegisterVariable(void* address, const char* name) {
|
||||||
|
register void* a0 asm("a0") = address;
|
||||||
|
register const char* a1 asm("a1") = name;
|
||||||
|
__asm__ volatile("sb %0, 0x2081(%1)" : : "r"(255), "r"(0x1f800000), "r"(a0), "r"(a1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Profiler::initialize() {
|
||||||
|
reset();
|
||||||
|
|
||||||
|
pcsxRegisterVariable(§ionTimes[0], "profiler.rendering");
|
||||||
|
pcsxRegisterVariable(§ionTimes[1], "profiler.lua");
|
||||||
|
pcsxRegisterVariable(§ionTimes[2], "profiler.controls");
|
||||||
|
pcsxRegisterVariable(§ionTimes[3], "profiler.navmesh");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Profiler::reset() {
|
||||||
|
for (auto &time : sectionTimes) {
|
||||||
|
time = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // PSXSPLASH_PROFILER
|
||||||
|
|
||||||
|
|
||||||
47
src/profiler.hh
Normal file
47
src/profiler.hh
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef PSXSPLASH_PROFILER
|
||||||
|
|
||||||
|
namespace psxsplash::debug {
|
||||||
|
|
||||||
|
enum ProfilerSection {
|
||||||
|
PROFILER_RENDERING,
|
||||||
|
PROFILER_LUA,
|
||||||
|
PROFILER_CONTROLS,
|
||||||
|
PROFILER_NAVMESH,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Profiler {
|
||||||
|
public:
|
||||||
|
// Singleton accessor
|
||||||
|
static Profiler& getInstance() {
|
||||||
|
static Profiler instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initialize();
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
void setSectionTime(ProfilerSection section, uint32_t time) {
|
||||||
|
sectionTimes[section] = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Profiler() = default;
|
||||||
|
~Profiler() = default;
|
||||||
|
|
||||||
|
// Delete copy/move semantics
|
||||||
|
Profiler(const Profiler&) = delete;
|
||||||
|
Profiler& operator=(const Profiler&) = delete;
|
||||||
|
Profiler(Profiler&&) = delete;
|
||||||
|
Profiler& operator=(Profiler&&) = delete;
|
||||||
|
|
||||||
|
uint32_t sectionTimes[4] = {0, 0, 0, 0};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash::debug
|
||||||
|
|
||||||
|
#endif // PSXSPLASH_PROFILER
|
||||||
854
src/renderer.cpp
854
src/renderer.cpp
@@ -1,146 +1,782 @@
|
|||||||
#include "renderer.hh"
|
#include "renderer.hh"
|
||||||
|
|
||||||
|
#include <EASTL/array.h>
|
||||||
#include <EASTL/vector.h>
|
#include <EASTL/vector.h>
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <psyqo/fixed-point.hh>
|
#include <psyqo/fixed-point.hh>
|
||||||
#include <psyqo/gte-kernels.hh>
|
#include <psyqo/gte-kernels.hh>
|
||||||
#include <psyqo/gte-registers.hh>
|
#include <psyqo/gte-registers.hh>
|
||||||
#include <psyqo/kernel.hh>
|
#include <psyqo/kernel.hh>
|
||||||
|
#include <psyqo/matrix.hh>
|
||||||
#include <psyqo/primitives/common.hh>
|
#include <psyqo/primitives/common.hh>
|
||||||
|
#include <psyqo/primitives/control.hh>
|
||||||
#include <psyqo/primitives/triangles.hh>
|
#include <psyqo/primitives/triangles.hh>
|
||||||
|
#include <psyqo/soft-math.hh>
|
||||||
|
#include <psyqo/trigonometry.hh>
|
||||||
#include <psyqo/vector.hh>
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
|
#include "gtemath.hh"
|
||||||
|
#include "uisystem.hh"
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
#include "memoverlay.hh"
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace psyqo::fixed_point_literals;
|
using namespace psyqo::fixed_point_literals;
|
||||||
using namespace psyqo::trig_literals;
|
using namespace psyqo::trig_literals;
|
||||||
|
using namespace psyqo::GTE;
|
||||||
|
|
||||||
psxsplash::Renderer* psxsplash::Renderer::instance = nullptr;
|
psxsplash::Renderer* psxsplash::Renderer::instance = nullptr;
|
||||||
|
|
||||||
void psxsplash::Renderer::init(psyqo::GPU &gpuInstance) {
|
void psxsplash::Renderer::Init(psyqo::GPU& gpuInstance) {
|
||||||
psyqo::Kernel::assert(instance == nullptr, "A second intialization of Renderer was tried");
|
psyqo::Kernel::assert(instance == nullptr,
|
||||||
|
"A second initialization of Renderer was tried");
|
||||||
|
clear<Register::TRX, Safe>();
|
||||||
|
clear<Register::TRY, Safe>();
|
||||||
|
clear<Register::TRZ, Safe>();
|
||||||
|
write<Register::OFX, Safe>(psyqo::FixedPoint<16>(160.0).raw());
|
||||||
|
write<Register::OFY, Safe>(psyqo::FixedPoint<16>(120.0).raw());
|
||||||
|
write<Register::H, Safe>(PROJ_H);
|
||||||
|
write<Register::ZSF3, Safe>(ORDERING_TABLE_SIZE / 3);
|
||||||
|
write<Register::ZSF4, Safe>(ORDERING_TABLE_SIZE / 4);
|
||||||
|
if (!instance) { instance = new Renderer(gpuInstance); }
|
||||||
|
}
|
||||||
|
|
||||||
psyqo::GTE::clear<psyqo::GTE::Register::TRX, psyqo::GTE::Unsafe>();
|
void psxsplash::Renderer::SetCamera(psxsplash::Camera& camera) { m_currentCamera = &camera; }
|
||||||
psyqo::GTE::clear<psyqo::GTE::Register::TRY, psyqo::GTE::Unsafe>();
|
|
||||||
psyqo::GTE::clear<psyqo::GTE::Register::TRZ, psyqo::GTE::Unsafe>();
|
|
||||||
|
|
||||||
psyqo::GTE::write<psyqo::GTE::Register::OFX, psyqo::GTE::Unsafe>(psyqo::FixedPoint<16>(160.0).raw());
|
void psxsplash::Renderer::SetFog(const FogConfig& fog) {
|
||||||
psyqo::GTE::write<psyqo::GTE::Register::OFY, psyqo::GTE::Unsafe>(psyqo::FixedPoint<16>(120.0).raw());
|
m_fog = fog;
|
||||||
|
// Always use fog color as the GPU clear/back color
|
||||||
psyqo::GTE::write<psyqo::GTE::Register::H, psyqo::GTE::Unsafe>(120);
|
m_clearcolor = fog.color;
|
||||||
|
if (fog.enabled) {
|
||||||
psyqo::GTE::write<psyqo::GTE::Register::ZSF3, psyqo::GTE::Unsafe>(ORDERING_TABLE_SIZE / 3);
|
write<Register::RFC, Unsafe>(static_cast<uint32_t>(fog.color.r) << 4);
|
||||||
psyqo::GTE::write<psyqo::GTE::Register::ZSF4, psyqo::GTE::Unsafe>(ORDERING_TABLE_SIZE / 4);
|
write<Register::GFC, Unsafe>(static_cast<uint32_t>(fog.color.g) << 4);
|
||||||
|
write<Register::BFC, Safe>(static_cast<uint32_t>(fog.color.b) << 4);
|
||||||
if (!instance) {
|
m_fog.fogFarSZ = 8000 / fog.density;
|
||||||
instance = new Renderer(gpuInstance);
|
} else {
|
||||||
|
m_fog.fogFarSZ = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void psxsplash::Renderer::setCamera(psxsplash::Camera &camera) { m_currentCamera = &camera; }
|
psyqo::Vec3 psxsplash::Renderer::computeCameraViewPos() {
|
||||||
|
::clear<Register::TRX, Safe>();
|
||||||
|
::clear<Register::TRY, Safe>();
|
||||||
|
::clear<Register::TRZ, Safe>();
|
||||||
|
writeSafe<PseudoRegister::Rotation>(m_currentCamera->GetRotation());
|
||||||
|
writeSafe<PseudoRegister::V0>(-m_currentCamera->GetPosition());
|
||||||
|
Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0, Kernels::TV::TR>();
|
||||||
|
return readSafe<PseudoRegister::SV>();
|
||||||
|
}
|
||||||
|
|
||||||
void psxsplash::Renderer::render(eastl::vector<GameObject *> &objects) {
|
void psxsplash::Renderer::setupObjectTransform(
|
||||||
|
GameObject* obj, const psyqo::Vec3& cameraPosition) {
|
||||||
|
::clear<Register::TRX, Safe>();
|
||||||
|
::clear<Register::TRY, Safe>();
|
||||||
|
::clear<Register::TRZ, Safe>();
|
||||||
|
writeSafe<PseudoRegister::Rotation>(m_currentCamera->GetRotation());
|
||||||
|
writeSafe<PseudoRegister::V0>(obj->position);
|
||||||
|
Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0, Kernels::TV::TR>();
|
||||||
|
psyqo::Vec3 objectPosition = readSafe<PseudoRegister::SV>();
|
||||||
|
objectPosition.x += cameraPosition.x;
|
||||||
|
objectPosition.y += cameraPosition.y;
|
||||||
|
objectPosition.z += cameraPosition.z;
|
||||||
|
psyqo::Matrix33 finalMatrix;
|
||||||
|
MatrixMultiplyGTE(m_currentCamera->GetRotation(), obj->rotation, &finalMatrix);
|
||||||
|
writeSafe<PseudoRegister::Translation>(objectPosition);
|
||||||
|
writeSafe<PseudoRegister::Rotation>(finalMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-vertex fog blend: result = vertexColor * (4096 - ir0) / 4096 + fogColor * ir0 / 4096
|
||||||
|
static inline psyqo::Color fogBlend(psyqo::Color vc, int32_t ir0, psyqo::Color fogC) {
|
||||||
|
if (ir0 <= 0) return vc;
|
||||||
|
if (ir0 >= 4096) return fogC;
|
||||||
|
int32_t keep = 4096 - ir0;
|
||||||
|
return {
|
||||||
|
.r = (uint8_t)((vc.r * keep + fogC.r * ir0) >> 12),
|
||||||
|
.g = (uint8_t)((vc.g * keep + fogC.g * ir0) >> 12),
|
||||||
|
.b = (uint8_t)((vc.b * keep + fogC.b * ir0) >> 12),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Core triangle pipeline (Bandwidth's proven approach + fog)
|
||||||
|
// rtpt -> nclip -> backface cull -> SZ depth -> SXY -> screen clip -> emit
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void psxsplash::Renderer::processTriangle(
|
||||||
|
Tri& tri, int32_t fogFarSZ,
|
||||||
|
psyqo::OrderingTable<ORDERING_TABLE_SIZE>& ot,
|
||||||
|
psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE>& balloc) {
|
||||||
|
|
||||||
|
writeSafe<PseudoRegister::V0>(tri.v0);
|
||||||
|
writeSafe<PseudoRegister::V1>(tri.v1);
|
||||||
|
writeSafe<PseudoRegister::V2>(tri.v2);
|
||||||
|
|
||||||
|
Kernels::rtpt();
|
||||||
|
|
||||||
|
uint32_t u0, u1, u2;
|
||||||
|
read<Register::SZ1>(&u0);
|
||||||
|
read<Register::SZ2>(&u1);
|
||||||
|
read<Register::SZ3>(&u2);
|
||||||
|
int32_t sz0 = (int32_t)u0, sz1 = (int32_t)u1, sz2 = (int32_t)u2;
|
||||||
|
|
||||||
|
if (sz0 < 1 && sz1 < 1 && sz2 < 1) return;
|
||||||
|
if (fogFarSZ > 0 && sz0 > fogFarSZ && sz1 > fogFarSZ && sz2 > fogFarSZ) return;
|
||||||
|
|
||||||
|
int32_t zIndex = eastl::max(eastl::max(sz0, sz1), sz2);
|
||||||
|
if (zIndex < 0 || zIndex >= (int32_t)ORDERING_TABLE_SIZE) return;
|
||||||
|
|
||||||
|
// Per-vertex fog: compute fog factor for each vertex individually based on
|
||||||
|
// its SZ depth. The GPU then interpolates the fogged colors smoothly across
|
||||||
|
// the triangle surface, eliminating the per-triangle tiling artifacts that
|
||||||
|
// occur when a single IR0 is used for the whole triangle.
|
||||||
|
//
|
||||||
|
// fogIR[i] = 0 means no fog (original color), 4096 = full fog (fog color).
|
||||||
|
// Quadratic ease-in curve: fog dominates over baked lighting quickly.
|
||||||
|
int32_t fogIR[3] = {0, 0, 0};
|
||||||
|
if (fogFarSZ > 0) {
|
||||||
|
int32_t fogNear = fogFarSZ / 4;
|
||||||
|
int32_t range = fogFarSZ - fogNear;
|
||||||
|
if (range < 1) range = 1;
|
||||||
|
int32_t szArr[3] = {sz0, sz1, sz2};
|
||||||
|
for (int vi = 0; vi < 3; vi++) {
|
||||||
|
int32_t ir;
|
||||||
|
if (szArr[vi] <= fogNear) {
|
||||||
|
ir = 0;
|
||||||
|
} else if (szArr[vi] >= fogFarSZ) {
|
||||||
|
ir = 4096;
|
||||||
|
} else {
|
||||||
|
// Linear 0..4096 over [fogNear, fogFarSZ]
|
||||||
|
int32_t t = ((szArr[vi] - fogNear) * 4096) / range;
|
||||||
|
// Quadratic ease-in: t^2 / 4096
|
||||||
|
ir = (t * t) >> 12;
|
||||||
|
}
|
||||||
|
fogIR[vi] = ir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
psyqo::Vertex projected[3];
|
||||||
|
read<Register::SXY0>(&projected[0].packed);
|
||||||
|
read<Register::SXY1>(&projected[1].packed);
|
||||||
|
read<Register::SXY2>(&projected[2].packed);
|
||||||
|
|
||||||
|
if (isCompletelyOutside(projected[0], projected[1], projected[2])) return;
|
||||||
|
|
||||||
|
// Triangles that need clipping skip nclip entirely.
|
||||||
|
// nclip with GTE-clamped screen coords gives wrong results for edge triangles.
|
||||||
|
// The clipper handles them directly - no backface cull needed since the
|
||||||
|
// clipper preserves winding and degenerate triangles produce zero-area output.
|
||||||
|
if (needsClipping(projected[0], projected[1], projected[2])) {
|
||||||
|
ClipVertex cv0 = {(int16_t)projected[0].x, (int16_t)projected[0].y, (int16_t)sz0,
|
||||||
|
tri.uvA.u, tri.uvA.v, tri.colorA.r, tri.colorA.g, tri.colorA.b};
|
||||||
|
ClipVertex cv1 = {(int16_t)projected[1].x, (int16_t)projected[1].y, (int16_t)sz1,
|
||||||
|
tri.uvB.u, tri.uvB.v, tri.colorB.r, tri.colorB.g, tri.colorB.b};
|
||||||
|
ClipVertex cv2 = {(int16_t)projected[2].x, (int16_t)projected[2].y, (int16_t)sz2,
|
||||||
|
tri.uvC.u, tri.uvC.v, tri.colorC.r, tri.colorC.g, tri.colorC.b};
|
||||||
|
ClipResult clipResult;
|
||||||
|
int clippedCount = clipTriangle(cv0, cv1, cv2, clipResult);
|
||||||
|
for (int ct = 0; ct < clippedCount; ct++) {
|
||||||
|
const ClipVertex& a = clipResult.verts[ct*3];
|
||||||
|
const ClipVertex& b = clipResult.verts[ct*3+1];
|
||||||
|
const ClipVertex& c = clipResult.verts[ct*3+2];
|
||||||
|
// For clipped vertices, use per-triangle fog (max SZ) since
|
||||||
|
// clipped vertex Z values may not map cleanly to the original SZs.
|
||||||
|
psyqo::Color ca = {a.r, a.g, a.b}, cb = {b.r, b.g, b.b}, cc = {c.r, c.g, c.b};
|
||||||
|
if (m_fog.enabled) {
|
||||||
|
int32_t maxIR = eastl::max(eastl::max(fogIR[0], fogIR[1]), fogIR[2]);
|
||||||
|
ca = fogBlend(ca, maxIR, m_fog.color);
|
||||||
|
cb = fogBlend(cb, maxIR, m_fog.color);
|
||||||
|
cc = fogBlend(cc, maxIR, m_fog.color);
|
||||||
|
}
|
||||||
|
if (tri.isUntextured()) {
|
||||||
|
auto& p = balloc.allocateFragment<psyqo::Prim::GouraudTriangle>();
|
||||||
|
p.primitive.pointA.x = a.x; p.primitive.pointA.y = a.y;
|
||||||
|
p.primitive.pointB.x = b.x; p.primitive.pointB.y = b.y;
|
||||||
|
p.primitive.pointC.x = c.x; p.primitive.pointC.y = c.y;
|
||||||
|
p.primitive.setColorA(ca); p.primitive.setColorB(cb); p.primitive.setColorC(cc);
|
||||||
|
p.primitive.setOpaque();
|
||||||
|
ot.insert(p, zIndex);
|
||||||
|
} else {
|
||||||
|
auto& p = balloc.allocateFragment<psyqo::Prim::GouraudTexturedTriangle>();
|
||||||
|
p.primitive.pointA.x = a.x; p.primitive.pointA.y = a.y;
|
||||||
|
p.primitive.pointB.x = b.x; p.primitive.pointB.y = b.y;
|
||||||
|
p.primitive.pointC.x = c.x; p.primitive.pointC.y = c.y;
|
||||||
|
p.primitive.uvA.u = a.u; p.primitive.uvA.v = a.v;
|
||||||
|
p.primitive.uvB.u = b.u; p.primitive.uvB.v = b.v;
|
||||||
|
p.primitive.uvC.u = c.u; p.primitive.uvC.v = c.v;
|
||||||
|
p.primitive.tpage = tri.tpage;
|
||||||
|
psyqo::PrimPieces::ClutIndex clut(tri.clutX, tri.clutY);
|
||||||
|
p.primitive.clutIndex = clut;
|
||||||
|
p.primitive.setColorA(ca); p.primitive.setColorB(cb); p.primitive.setColorC(cc);
|
||||||
|
p.primitive.setOpaque();
|
||||||
|
ot.insert(p, zIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal path: triangle is fully inside clip region with safe deltas.
|
||||||
|
// nclip is reliable here since screen coords aren't clamped.
|
||||||
|
Kernels::nclip();
|
||||||
|
int32_t mac0 = 0;
|
||||||
|
read<Register::MAC0>(reinterpret_cast<uint32_t*>(&mac0));
|
||||||
|
if (mac0 <= 0) return;
|
||||||
|
|
||||||
|
// Per-vertex fog: blend each vertex color toward fog color based on its depth.
|
||||||
|
// GPU interpolates these smoothly across the triangle - no tiling artifacts.
|
||||||
|
psyqo::Color cA = tri.colorA, cB = tri.colorB, cC = tri.colorC;
|
||||||
|
if (m_fog.enabled) {
|
||||||
|
cA = fogBlend(cA, fogIR[0], m_fog.color);
|
||||||
|
cB = fogBlend(cB, fogIR[1], m_fog.color);
|
||||||
|
cC = fogBlend(cC, fogIR[2], m_fog.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tri.isUntextured()) {
|
||||||
|
auto& p = balloc.allocateFragment<psyqo::Prim::GouraudTriangle>();
|
||||||
|
p.primitive.pointA = projected[0]; p.primitive.pointB = projected[1]; p.primitive.pointC = projected[2];
|
||||||
|
p.primitive.setColorA(cA); p.primitive.setColorB(cB); p.primitive.setColorC(cC);
|
||||||
|
p.primitive.setOpaque();
|
||||||
|
ot.insert(p, zIndex);
|
||||||
|
} else {
|
||||||
|
auto& p = balloc.allocateFragment<psyqo::Prim::GouraudTexturedTriangle>();
|
||||||
|
p.primitive.pointA = projected[0]; p.primitive.pointB = projected[1]; p.primitive.pointC = projected[2];
|
||||||
|
p.primitive.uvA = tri.uvA; p.primitive.uvB = tri.uvB; p.primitive.uvC = tri.uvC;
|
||||||
|
p.primitive.tpage = tri.tpage;
|
||||||
|
psyqo::PrimPieces::ClutIndex clut(tri.clutX, tri.clutY);
|
||||||
|
p.primitive.clutIndex = clut;
|
||||||
|
p.primitive.setColorA(cA); p.primitive.setColorB(cB); p.primitive.setColorC(cC);
|
||||||
|
p.primitive.setOpaque();
|
||||||
|
ot.insert(p, zIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Render paths
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void psxsplash::Renderer::Render(eastl::vector<GameObject*>& objects) {
|
||||||
psyqo::Kernel::assert(m_currentCamera != nullptr, "PSXSPLASH: Tried to render without an active camera");
|
psyqo::Kernel::assert(m_currentCamera != nullptr, "PSXSPLASH: Tried to render without an active camera");
|
||||||
|
uint8_t parity = m_gpu.getParity();
|
||||||
|
auto& ot = m_ots[parity]; auto& clear = m_clear[parity]; auto& balloc = m_ballocs[parity];
|
||||||
|
balloc.reset();
|
||||||
|
// Set dithering draw mode at the back of the OT so it fires before any geometry.
|
||||||
|
auto& ditherCmd = balloc.allocateFragment<psyqo::Prim::TPage>();
|
||||||
|
ditherCmd.primitive.attr.setDithering(true);
|
||||||
|
ot.insert(ditherCmd, ORDERING_TABLE_SIZE - 1);
|
||||||
|
|
||||||
|
psyqo::Vec3 cameraPosition = computeCameraViewPos();
|
||||||
|
int32_t fogFarSZ = m_fog.fogFarSZ;
|
||||||
|
for (auto& obj : objects) {
|
||||||
|
if (!obj->isActive()) continue;
|
||||||
|
setupObjectTransform(obj, cameraPosition);
|
||||||
|
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);
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
if (m_memOverlay) m_memOverlay->renderOT(ot, balloc);
|
||||||
|
#endif
|
||||||
|
m_gpu.getNextClear(clear.primitive, m_clearcolor);
|
||||||
|
m_gpu.chain(clear); m_gpu.chain(ot);
|
||||||
|
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
if (m_memOverlay) m_memOverlay->renderText(m_gpu);
|
||||||
|
#endif
|
||||||
|
m_frameCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Renderer::RenderWithBVH(eastl::vector<GameObject*>& objects, const BVHManager& bvh) {
|
||||||
|
psyqo::Kernel::assert(m_currentCamera != nullptr, "PSXSPLASH: Tried to render without an active camera");
|
||||||
|
if (!bvh.isLoaded()) { Render(objects); return; }
|
||||||
|
uint8_t parity = m_gpu.getParity();
|
||||||
|
auto& ot = m_ots[parity]; auto& clear = m_clear[parity]; auto& balloc = m_ballocs[parity];
|
||||||
|
balloc.reset();
|
||||||
|
auto& ditherCmd2 = balloc.allocateFragment<psyqo::Prim::TPage>();
|
||||||
|
ditherCmd2.primitive.attr.setDithering(true);
|
||||||
|
ot.insert(ditherCmd2, ORDERING_TABLE_SIZE - 1);
|
||||||
|
|
||||||
|
Frustum frustum; m_currentCamera->ExtractFrustum(frustum);
|
||||||
|
int visibleCount = bvh.cullFrustum(frustum, m_visibleRefs, MAX_VISIBLE_TRIANGLES);
|
||||||
|
psyqo::Vec3 cameraPosition = computeCameraViewPos();
|
||||||
|
int32_t fogFarSZ = m_fog.fogFarSZ;
|
||||||
|
int16_t lastObjectIndex = -1;
|
||||||
|
for (int i = 0; i < visibleCount; i++) {
|
||||||
|
const TriangleRef& ref = m_visibleRefs[i];
|
||||||
|
if (ref.objectIndex >= objects.size()) continue;
|
||||||
|
GameObject* obj = objects[ref.objectIndex];
|
||||||
|
if (!obj->isActive()) continue;
|
||||||
|
if (ref.triangleIndex >= obj->polyCount) continue;
|
||||||
|
if (ref.objectIndex != lastObjectIndex) {
|
||||||
|
lastObjectIndex = ref.objectIndex;
|
||||||
|
setupObjectTransform(obj, cameraPosition);
|
||||||
|
}
|
||||||
|
processTriangle(obj->polygons[ref.triangleIndex], fogFarSZ, ot, balloc);
|
||||||
|
}
|
||||||
|
if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc);
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
if (m_memOverlay) m_memOverlay->renderOT(ot, balloc);
|
||||||
|
#endif
|
||||||
|
m_gpu.getNextClear(clear.primitive, m_clearcolor);
|
||||||
|
m_gpu.chain(clear); m_gpu.chain(ot);
|
||||||
|
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
if (m_memOverlay) m_memOverlay->renderText(m_gpu);
|
||||||
|
#endif
|
||||||
|
m_frameCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RenderWithRooms - Portal/room occlusion for interior scenes
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
struct ScreenRect { int16_t minX, minY, maxX, maxY; };
|
||||||
|
|
||||||
|
static inline bool intersectRect(const ScreenRect& a, const ScreenRect& b, ScreenRect& out) {
|
||||||
|
out.minX = (a.minX > b.minX) ? a.minX : b.minX; out.minY = (a.minY > b.minY) ? a.minY : b.minY;
|
||||||
|
out.maxX = (a.maxX < b.maxX) ? a.maxX : b.maxX; out.maxY = (a.maxY < b.maxY) ? a.maxY : b.maxY;
|
||||||
|
return out.minX < out.maxX && out.minY < out.maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety margin added to portal screen rects (pixels).
|
||||||
|
// Prevents geometry from popping at portal edges due to fixed-point rounding.
|
||||||
|
static constexpr int16_t PORTAL_MARGIN = 16;
|
||||||
|
|
||||||
|
// Transform a world-space point to camera space using the view rotation matrix.
|
||||||
|
static inline void worldToCamera(int32_t wx, int32_t wy, int32_t wz,
|
||||||
|
int32_t camX, int32_t camY, int32_t camZ,
|
||||||
|
const psyqo::Matrix33& camRot,
|
||||||
|
int32_t& outX, int32_t& outY, int32_t& outZ) {
|
||||||
|
int32_t rx = wx - camX, ry = wy - camY, rz = wz - camZ;
|
||||||
|
outX = (int32_t)(((int64_t)camRot.vs[0].x.value * rx + (int64_t)camRot.vs[0].y.value * ry +
|
||||||
|
(int64_t)camRot.vs[0].z.value * rz) >> 12);
|
||||||
|
outY = (int32_t)(((int64_t)camRot.vs[1].x.value * rx + (int64_t)camRot.vs[1].y.value * ry +
|
||||||
|
(int64_t)camRot.vs[1].z.value * rz) >> 12);
|
||||||
|
outZ = (int32_t)(((int64_t)camRot.vs[2].x.value * rx + (int64_t)camRot.vs[2].y.value * ry +
|
||||||
|
(int64_t)camRot.vs[2].z.value * rz) >> 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project a camera-space point to screen coordinates.
|
||||||
|
// Returns false if behind near plane.
|
||||||
|
static inline bool projectToScreen(int32_t vx, int32_t vy, int32_t vz,
|
||||||
|
int16_t& sx, int16_t& sy) {
|
||||||
|
if (vz <= 0) return false;
|
||||||
|
constexpr int32_t H = 120;
|
||||||
|
int32_t vzs = vz >> 4; if (vzs <= 0) vzs = 1;
|
||||||
|
sx = (int16_t)((vx >> 4) * H / vzs + 160);
|
||||||
|
sy = (int16_t)((vy >> 4) * H / vzs + 120);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project a portal quad to a screen-space AABB.
|
||||||
|
// Computes the 4 corners, transforms to camera space, clips against the near plane,
|
||||||
|
// projects visible points to screen, and returns the bounding rect.
|
||||||
|
static bool projectPortalRect(const psxsplash::PortalData& portal,
|
||||||
|
int32_t camX, int32_t camY, int32_t camZ, const psyqo::Matrix33& camRot, ScreenRect& outRect) {
|
||||||
|
|
||||||
|
// Compute portal corner offsets in world space.
|
||||||
|
int32_t rwx = ((int32_t)portal.rightX * portal.halfW) >> 12;
|
||||||
|
int32_t rwy = ((int32_t)portal.rightY * portal.halfW) >> 12;
|
||||||
|
int32_t rwz = ((int32_t)portal.rightZ * portal.halfW) >> 12;
|
||||||
|
int32_t uhx = ((int32_t)portal.upX * portal.halfH) >> 12;
|
||||||
|
int32_t uhy = ((int32_t)portal.upY * portal.halfH) >> 12;
|
||||||
|
int32_t uhz = ((int32_t)portal.upZ * portal.halfH) >> 12;
|
||||||
|
|
||||||
|
int32_t cx = portal.centerX, cy = portal.centerY, cz = portal.centerZ;
|
||||||
|
|
||||||
|
// Transform 4 corners to camera space
|
||||||
|
struct CamVert { int32_t x, y, z; };
|
||||||
|
CamVert cv[4];
|
||||||
|
int32_t wCorners[4][3] = {
|
||||||
|
{cx + rwx + uhx, cy + rwy + uhy, cz + rwz + uhz},
|
||||||
|
{cx - rwx + uhx, cy - rwy + uhy, cz - rwz + uhz},
|
||||||
|
{cx - rwx - uhx, cy - rwy - uhy, cz - rwz - uhz},
|
||||||
|
{cx + rwx - uhx, cy + rwy - uhy, cz + rwz - uhz},
|
||||||
|
};
|
||||||
|
|
||||||
|
int behindCount = 0;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
worldToCamera(wCorners[i][0], wCorners[i][1], wCorners[i][2],
|
||||||
|
camX, camY, camZ, camRot, cv[i].x, cv[i].y, cv[i].z);
|
||||||
|
if (cv[i].z <= 0) behindCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (behindCount == 4) {
|
||||||
|
// All corners behind camera. Only allow if camera is very close to portal.
|
||||||
|
int32_t vx, vy, vz;
|
||||||
|
worldToCamera(cx, cy, cz, camX, camY, camZ, camRot, vx, vy, vz);
|
||||||
|
int32_t portalExtent = portal.halfW > portal.halfH ? portal.halfW : portal.halfH;
|
||||||
|
if (-vz > portalExtent * 2) return false;
|
||||||
|
outRect = {-512, -512, 832, 752};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip against near plane (z=1) and project visible points.
|
||||||
|
// For each edge where one vertex is in front and one behind,
|
||||||
|
// compute the intersection point and include it in the screen rect.
|
||||||
|
constexpr int32_t NEAR_Z = 1;
|
||||||
|
int16_t sxMin = 32767, sxMax = -32768;
|
||||||
|
int16_t syMin = 32767, syMax = -32768;
|
||||||
|
int projCount = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int j = (i + 1) % 4;
|
||||||
|
|
||||||
|
// Project vertex i if in front
|
||||||
|
if (cv[i].z > 0) {
|
||||||
|
int16_t sx, sy;
|
||||||
|
if (projectToScreen(cv[i].x, cv[i].y, cv[i].z, sx, sy)) {
|
||||||
|
if (sx < sxMin) sxMin = sx;
|
||||||
|
if (sx > sxMax) sxMax = sx;
|
||||||
|
if (sy < syMin) syMin = sy;
|
||||||
|
if (sy > syMax) syMax = sy;
|
||||||
|
projCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If edge crosses the near plane, clip and project the intersection.
|
||||||
|
// All 32-bit arithmetic (no __divdi3 on MIPS R3000).
|
||||||
|
bool iFront = cv[i].z > 0;
|
||||||
|
bool jFront = cv[j].z > 0;
|
||||||
|
if (iFront != jFront) {
|
||||||
|
int32_t dz = cv[j].z - cv[i].z;
|
||||||
|
if (dz == 0) continue;
|
||||||
|
int32_t dzs = dz >> 4;
|
||||||
|
if (dzs == 0) dzs = (dz > 0) ? 1 : -1; // prevent div-by-zero after shift
|
||||||
|
// Compute t in 4.12 fixed-point. Shift num/den by 4 to keep * 4096 in 32 bits.
|
||||||
|
int32_t t12 = (((NEAR_Z - cv[i].z) >> 4) * 4096) / dzs;
|
||||||
|
// Apply t: clip = cv[i] + (cv[j] - cv[i]) * t12 / 4096
|
||||||
|
// Shift dx by 4 so (dx>>4)*t12 fits int32, then >>8 to undo (4+8=12 total)
|
||||||
|
int32_t clipX = cv[i].x + ((((cv[j].x - cv[i].x) >> 4) * t12) >> 8);
|
||||||
|
int32_t clipY = cv[i].y + ((((cv[j].y - cv[i].y) >> 4) * t12) >> 8);
|
||||||
|
int16_t sx, sy;
|
||||||
|
if (projectToScreen(clipX, clipY, NEAR_Z, sx, sy)) {
|
||||||
|
if (sx < sxMin) sxMin = sx;
|
||||||
|
if (sx > sxMax) sxMax = sx;
|
||||||
|
if (sy < syMin) syMin = sy;
|
||||||
|
if (sy > syMax) syMax = sy;
|
||||||
|
projCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projCount == 0) return false;
|
||||||
|
|
||||||
|
outRect = {
|
||||||
|
(int16_t)(sxMin - PORTAL_MARGIN), (int16_t)(syMin - PORTAL_MARGIN),
|
||||||
|
(int16_t)(sxMax + PORTAL_MARGIN), (int16_t)(syMax + PORTAL_MARGIN)
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test if a room's AABB is potentially visible to the camera frustum.
|
||||||
|
// Quick rejection test: if the room is entirely behind the camera, skip it.
|
||||||
|
static bool isRoomPotentiallyVisible(const psxsplash::RoomData& room,
|
||||||
|
int32_t camX, int32_t camY, int32_t camZ, const psyqo::Matrix33& camRot) {
|
||||||
|
// Transform the room's AABB center to camera space and check Z.
|
||||||
|
// Use the p-vertex approach: find the corner most in the camera forward direction.
|
||||||
|
int32_t fwdX = camRot.vs[2].x.value;
|
||||||
|
int32_t fwdY = camRot.vs[2].y.value;
|
||||||
|
int32_t fwdZ = camRot.vs[2].z.value;
|
||||||
|
|
||||||
|
// p-vertex: corner of AABB closest to camera forward direction
|
||||||
|
int32_t px = (fwdX >= 0) ? room.aabbMaxX : room.aabbMinX;
|
||||||
|
int32_t py = (fwdY >= 0) ? room.aabbMaxY : room.aabbMinY;
|
||||||
|
int32_t pz = (fwdZ >= 0) ? room.aabbMaxZ : room.aabbMinZ;
|
||||||
|
|
||||||
|
// If p-vertex is behind camera, the entire AABB is behind
|
||||||
|
int32_t rx = px - camX, ry = py - camY, rz = pz - camZ;
|
||||||
|
int64_t dotFwd = ((int64_t)fwdX * rx + (int64_t)fwdY * ry + (int64_t)fwdZ * rz) >> 12;
|
||||||
|
if (dotFwd < -4096) return false; // Entirely behind with 1-unit margin
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Renderer::RenderWithRooms(eastl::vector<GameObject*>& objects,
|
||||||
|
const RoomData* rooms, int roomCount, const PortalData* portals, int portalCount,
|
||||||
|
const TriangleRef* roomTriRefs, int cameraRoom) {
|
||||||
|
psyqo::Kernel::assert(m_currentCamera != nullptr, "PSXSPLASH: Tried to render without an active camera");
|
||||||
|
if (roomCount == 0 || rooms == nullptr) { Render(objects); return; }
|
||||||
|
|
||||||
uint8_t parity = m_gpu.getParity();
|
uint8_t parity = m_gpu.getParity();
|
||||||
|
auto& ot = m_ots[parity]; auto& clear = m_clear[parity]; auto& balloc = m_ballocs[parity];
|
||||||
auto &ot = m_ots[parity];
|
|
||||||
auto &clear = m_clear[parity];
|
|
||||||
auto &balloc = m_ballocs[parity];
|
|
||||||
|
|
||||||
eastl::array<psyqo::Vertex, 3> projected;
|
|
||||||
|
|
||||||
m_gpu.getNextClear(clear.primitive, m_clearcolor);
|
|
||||||
m_gpu.chain(clear);
|
|
||||||
|
|
||||||
balloc.reset();
|
balloc.reset();
|
||||||
|
auto& ditherCmd3 = balloc.allocateFragment<psyqo::Prim::TPage>();
|
||||||
|
ditherCmd3.primitive.attr.setDithering(true);
|
||||||
|
ot.insert(ditherCmd3, ORDERING_TABLE_SIZE - 1);
|
||||||
|
|
||||||
for (auto &obj : objects) {
|
psyqo::Vec3 cameraPosition = computeCameraViewPos();
|
||||||
for (int i = 0; i < obj->polyCount; i++) {
|
int32_t fogFarSZ = m_fog.fogFarSZ;
|
||||||
Tri &tri = obj->polygons[i];
|
int32_t camX = m_currentCamera->GetPosition().x.raw();
|
||||||
psyqo::Vec3 result;
|
int32_t camY = m_currentCamera->GetPosition().y.raw();
|
||||||
|
int32_t camZ = m_currentCamera->GetPosition().z.raw();
|
||||||
|
int catchAllIdx = roomCount - 1;
|
||||||
|
|
||||||
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::Rotation>(obj->rotation);
|
// If no camera room provided (or invalid), fall back to AABB containment.
|
||||||
psyqo::GTE::write<psyqo::GTE::Register::TRX, psyqo::GTE::Unsafe>(obj->position.x.raw() +
|
// Pick the smallest room whose AABB (with margin) contains the camera.
|
||||||
m_currentCamera->getPosition().x.raw());
|
if (cameraRoom < 0 || cameraRoom >= catchAllIdx) {
|
||||||
psyqo::GTE::write<psyqo::GTE::Register::TRY, psyqo::GTE::Unsafe>(obj->position.y.raw() +
|
constexpr int32_t ROOM_MARGIN = 2048; // 0.5 units in fp12
|
||||||
m_currentCamera->getPosition().y.raw());
|
int64_t bestVolume = 0x7FFFFFFFFFFFFFFFLL;
|
||||||
psyqo::GTE::write<psyqo::GTE::Register::TRZ, psyqo::GTE::Safe>(obj->position.z.raw() +
|
for (int r = 0; r < catchAllIdx; r++) {
|
||||||
m_currentCamera->getPosition().z.raw());
|
if (camX >= rooms[r].aabbMinX - ROOM_MARGIN && camX <= rooms[r].aabbMaxX + ROOM_MARGIN &&
|
||||||
|
camY >= rooms[r].aabbMinY - ROOM_MARGIN && camY <= rooms[r].aabbMaxY + ROOM_MARGIN &&
|
||||||
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V0>(tri.v0);
|
camZ >= rooms[r].aabbMinZ - ROOM_MARGIN && camZ <= rooms[r].aabbMaxZ + ROOM_MARGIN) {
|
||||||
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V1>(tri.v1);
|
int64_t dx = (int64_t)(rooms[r].aabbMaxX - rooms[r].aabbMinX);
|
||||||
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V2>(tri.v2);
|
int64_t dy = (int64_t)(rooms[r].aabbMaxY - rooms[r].aabbMinY);
|
||||||
|
int64_t dz = (int64_t)(rooms[r].aabbMaxZ - rooms[r].aabbMinZ);
|
||||||
psyqo::GTE::Kernels::mvmva<psyqo::GTE::Kernels::MX::RT, psyqo::GTE::Kernels::MV::V0,
|
int64_t vol = dx * dy + dy * dz + dx * dz;
|
||||||
psyqo::GTE::Kernels::TV::TR>();
|
if (vol < bestVolume) { bestVolume = vol; cameraRoom = r; }
|
||||||
result = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
|
}
|
||||||
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V0>(result);
|
|
||||||
|
|
||||||
psyqo::GTE::Kernels::mvmva<psyqo::GTE::Kernels::MX::RT, psyqo::GTE::Kernels::MV::V1,
|
|
||||||
psyqo::GTE::Kernels::TV::TR>();
|
|
||||||
result = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
|
|
||||||
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V1>(result);
|
|
||||||
|
|
||||||
psyqo::GTE::Kernels::mvmva<psyqo::GTE::Kernels::MX::RT, psyqo::GTE::Kernels::MV::V2,
|
|
||||||
psyqo::GTE::Kernels::TV::TR>();
|
|
||||||
result = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
|
|
||||||
psyqo::GTE::writeSafe<psyqo::GTE::PseudoRegister::V2>(result);
|
|
||||||
|
|
||||||
psyqo::GTE::writeUnsafe<psyqo::GTE::PseudoRegister::Rotation>(m_currentCamera->getRotation());
|
|
||||||
psyqo::GTE::clear<psyqo::GTE::Register::TRX, psyqo::GTE::Unsafe>();
|
|
||||||
psyqo::GTE::clear<psyqo::GTE::Register::TRY, psyqo::GTE::Unsafe>();
|
|
||||||
psyqo::GTE::clear<psyqo::GTE::Register::TRZ, psyqo::GTE::Unsafe>();
|
|
||||||
|
|
||||||
psyqo::GTE::Kernels::rtpt();
|
|
||||||
psyqo::GTE::Kernels::nclip();
|
|
||||||
|
|
||||||
int32_t mac0 = 0;
|
|
||||||
psyqo::GTE::read<psyqo::GTE::Register::MAC0>(reinterpret_cast<uint32_t *>(&mac0));
|
|
||||||
if (mac0 <= 0) continue;
|
|
||||||
|
|
||||||
int32_t zIndex = 0;
|
|
||||||
uint32_t sz0, sz1, sz2;
|
|
||||||
psyqo::GTE::read<psyqo::GTE::Register::SZ0>(&sz0);
|
|
||||||
psyqo::GTE::read<psyqo::GTE::Register::SZ1>(&sz1);
|
|
||||||
psyqo::GTE::read<psyqo::GTE::Register::SZ2>(&sz2);
|
|
||||||
|
|
||||||
zIndex = eastl::max(eastl::max(sz0, sz1), sz2);
|
|
||||||
|
|
||||||
// psyqo::GTE::read<psyqo::GTE::Register::OTZ>(reinterpret_cast<uint32_t *>(&zIndex));
|
|
||||||
|
|
||||||
if (zIndex < 0 || zIndex >= ORDERING_TABLE_SIZE) continue;
|
|
||||||
|
|
||||||
psyqo::GTE::read<psyqo::GTE::Register::SXY0>(&projected[0].packed);
|
|
||||||
psyqo::GTE::read<psyqo::GTE::Register::SXY1>(&projected[1].packed);
|
|
||||||
psyqo::GTE::read<psyqo::GTE::Register::SXY2>(&projected[2].packed);
|
|
||||||
|
|
||||||
auto &prim = balloc.allocateFragment<psyqo::Prim::GouraudTexturedTriangle>();
|
|
||||||
|
|
||||||
psyqo::PrimPieces::ClutIndex clut(obj->clutX, obj->clutY);
|
|
||||||
|
|
||||||
prim.primitive.pointA = projected[0];
|
|
||||||
prim.primitive.pointB = projected[1];
|
|
||||||
prim.primitive.pointC = projected[2];
|
|
||||||
prim.primitive.uvA = tri.uvA;
|
|
||||||
prim.primitive.uvB = tri.uvB;
|
|
||||||
prim.primitive.uvC = tri.uvC;
|
|
||||||
prim.primitive.tpage = obj->texture;
|
|
||||||
prim.primitive.clutIndex = clut;
|
|
||||||
prim.primitive.setColorA(tri.colorA);
|
|
||||||
prim.primitive.setColorB(tri.colorB);
|
|
||||||
prim.primitive.setColorC(tri.colorC);
|
|
||||||
|
|
||||||
prim.primitive.setOpaque();
|
|
||||||
|
|
||||||
ot.insert(prim, zIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_gpu.chain(ot);
|
uint32_t visited = 0;
|
||||||
|
if (catchAllIdx < 32) visited = (1u << catchAllIdx);
|
||||||
|
const auto& camRot = m_currentCamera->GetRotation();
|
||||||
|
|
||||||
|
struct Entry { int room; int depth; ScreenRect clip; };
|
||||||
|
Entry stack[64]; int top = 0;
|
||||||
|
|
||||||
|
auto renderRoom = [&](int ri) {
|
||||||
|
const RoomData& rm = rooms[ri];
|
||||||
|
int16_t lastObj = -1;
|
||||||
|
for (int ti = 0; ti < rm.triRefCount; ti++) {
|
||||||
|
const TriangleRef& ref = roomTriRefs[rm.firstTriRef + ti];
|
||||||
|
if (ref.objectIndex >= objects.size()) continue;
|
||||||
|
GameObject* obj = objects[ref.objectIndex];
|
||||||
|
if (!obj->isActive()) continue;
|
||||||
|
if (ref.triangleIndex >= obj->polyCount) continue;
|
||||||
|
if (ref.objectIndex != lastObj) { lastObj = ref.objectIndex; setupObjectTransform(obj, cameraPosition); }
|
||||||
|
processTriangle(obj->polygons[ref.triangleIndex], fogFarSZ, ot, balloc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always render catch-all room (geometry not assigned to any specific room)
|
||||||
|
renderRoom(catchAllIdx);
|
||||||
|
|
||||||
|
if (cameraRoom >= 0) {
|
||||||
|
ScreenRect full = {-512, -512, 832, 752};
|
||||||
|
if (cameraRoom < 32) visited |= (1u << cameraRoom);
|
||||||
|
stack[top++] = {cameraRoom, 0, full};
|
||||||
|
while (top > 0) {
|
||||||
|
Entry e = stack[--top];
|
||||||
|
renderRoom(e.room);
|
||||||
|
if (e.depth >= 8) continue; // Depth limit prevents infinite loops
|
||||||
|
for (int p = 0; p < portalCount; p++) {
|
||||||
|
int other = -1;
|
||||||
|
if (portals[p].roomA == e.room) other = portals[p].roomB;
|
||||||
|
else if (portals[p].roomB == e.room) other = portals[p].roomA;
|
||||||
|
else continue;
|
||||||
|
if (other < 0 || other >= roomCount) continue;
|
||||||
|
if (other < 32 && (visited & (1u << other))) continue;
|
||||||
|
|
||||||
|
// Backface cull: skip portals that face away from the camera.
|
||||||
|
// The portal normal points from roomA toward roomB (4.12 fp).
|
||||||
|
// dot(normal, cam - portalCenter) > 0 means the portal faces us when
|
||||||
|
// traversing A->B; the sign flips when traversing B->A.
|
||||||
|
{
|
||||||
|
int32_t dx = camX - portals[p].centerX;
|
||||||
|
int32_t dy = camY - portals[p].centerY;
|
||||||
|
int32_t dz = camZ - portals[p].centerZ;
|
||||||
|
int64_t dot = (int64_t)dx * portals[p].normalX +
|
||||||
|
(int64_t)dy * portals[p].normalY +
|
||||||
|
(int64_t)dz * portals[p].normalZ;
|
||||||
|
// Allow a small negative threshold so nearly-edge-on portals still pass.
|
||||||
|
const int64_t BACKFACE_THRESHOLD = -4096;
|
||||||
|
if (portals[p].roomA == e.room) {
|
||||||
|
if (dot < BACKFACE_THRESHOLD) continue;
|
||||||
|
} else {
|
||||||
|
if (dot > -BACKFACE_THRESHOLD) continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void psxsplash::Renderer::vramUpload(const uint16_t *imageData, int16_t posX, int16_t posY, int16_t width,
|
// Phase 4: Frustum-cull the destination room's AABB.
|
||||||
int16_t height) {
|
// If the room is entirely behind the camera, skip.
|
||||||
|
if (!isRoomPotentiallyVisible(rooms[other], camX, camY, camZ, camRot)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Project actual portal quad corners to screen.
|
||||||
|
ScreenRect pr;
|
||||||
|
if (!projectPortalRect(portals[p], camX, camY, camZ, camRot, pr)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ScreenRect isect;
|
||||||
|
if (!intersectRect(e.clip, pr, isect)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (other < 32) visited |= (1u << other);
|
||||||
|
if (top < 64) stack[top++] = {other, e.depth + 1, isect};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Camera room unknown - render ALL rooms as safety fallback.
|
||||||
|
// This guarantees no geometry disappears, at the cost of no culling.
|
||||||
|
for (int r = 0; r < roomCount; r++) if (r != catchAllIdx) renderRoom(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef PSXSPLASH_ROOM_DEBUG
|
||||||
|
// ================================================================
|
||||||
|
// Debug overlay: room status bars + portal outlines
|
||||||
|
// ================================================================
|
||||||
|
{
|
||||||
|
static const psyqo::Color roomColors[] = {
|
||||||
|
{.r = 255, .g = 50, .b = 50}, // R0: red
|
||||||
|
{.r = 50, .g = 255, .b = 50}, // R1: green
|
||||||
|
{.r = 50, .g = 50, .b = 255}, // R2: blue
|
||||||
|
{.r = 255, .g = 255, .b = 50}, // R3: yellow
|
||||||
|
{.r = 255, .g = 50, .b = 255}, // R4: magenta
|
||||||
|
{.r = 50, .g = 255, .b = 255}, // R5: cyan
|
||||||
|
{.r = 255, .g = 128, .b = 50}, // R6: orange
|
||||||
|
{.r = 128, .g = 128, .b = 255}, // R7: lavender
|
||||||
|
};
|
||||||
|
|
||||||
|
// Room status bars at top of screen
|
||||||
|
for (int r = 0; r < roomCount && r < 8; r++) {
|
||||||
|
bool rendered = (visited & (1u << r)) != 0;
|
||||||
|
bool isCamRoom = (r == cameraRoom);
|
||||||
|
auto& tile = balloc.allocateFragment<psyqo::Prim::FastFill>();
|
||||||
|
int16_t x = r * 18 + 2;
|
||||||
|
tile.primitive.setColor(rendered ?
|
||||||
|
roomColors[r] : psyqo::Color{.r = 40, .g = 40, .b = 40});
|
||||||
|
tile.primitive.rect = psyqo::Rect{
|
||||||
|
.a = {.x = x, .y = (int16_t)2},
|
||||||
|
.b = {.w = 14, .h = (int16_t)(isCamRoom ? 12 : 6)}
|
||||||
|
};
|
||||||
|
ot.insert(tile, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Portal outlines: project portal quad and draw edges as thin lines.
|
||||||
|
// Lines are drawn at OT front (depth 0) so they show through walls.
|
||||||
|
for (int p = 0; p < portalCount; p++) {
|
||||||
|
const PortalData& portal = portals[p];
|
||||||
|
|
||||||
|
// Compute portal corners in world space
|
||||||
|
int32_t rwx = ((int32_t)portal.rightX * portal.halfW) >> 12;
|
||||||
|
int32_t rwy = ((int32_t)portal.rightY * portal.halfW) >> 12;
|
||||||
|
int32_t rwz = ((int32_t)portal.rightZ * portal.halfW) >> 12;
|
||||||
|
int32_t uhx = ((int32_t)portal.upX * portal.halfH) >> 12;
|
||||||
|
int32_t uhy = ((int32_t)portal.upY * portal.halfH) >> 12;
|
||||||
|
int32_t uhz = ((int32_t)portal.upZ * portal.halfH) >> 12;
|
||||||
|
|
||||||
|
int32_t cx = portal.centerX, cy = portal.centerY, cz = portal.centerZ;
|
||||||
|
struct { int32_t wx, wy, wz; } corners[4] = {
|
||||||
|
{cx + rwx + uhx, cy + rwy + uhy, cz + rwz + uhz},
|
||||||
|
{cx - rwx + uhx, cy - rwy + uhy, cz - rwz + uhz},
|
||||||
|
{cx - rwx - uhx, cy - rwy - uhy, cz - rwz - uhz},
|
||||||
|
{cx + rwx - uhx, cy + rwy - uhy, cz + rwz - uhz},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Project corners to screen
|
||||||
|
int16_t sx[4], sy[4];
|
||||||
|
bool vis[4];
|
||||||
|
int visCount = 0;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int32_t vx, vy, vz;
|
||||||
|
worldToCamera(corners[i].wx, corners[i].wy, corners[i].wz,
|
||||||
|
camX, camY, camZ, camRot, vx, vy, vz);
|
||||||
|
vis[i] = projectToScreen(vx, vy, vz, sx[i], sy[i]);
|
||||||
|
if (vis[i]) visCount++;
|
||||||
|
}
|
||||||
|
if (visCount < 2) continue; // Can't draw edges with <2 visible corners
|
||||||
|
|
||||||
|
// Draw each edge as a degenerate triangle (line).
|
||||||
|
// Color: orange for portal between visible rooms, dim for invisible.
|
||||||
|
bool portalActive = (visited & (1u << portal.roomA)) || (visited & (1u << portal.roomB));
|
||||||
|
psyqo::Color lineColor = portalActive ?
|
||||||
|
psyqo::Color{.r = 255, .g = 160, .b = 0} :
|
||||||
|
psyqo::Color{.r = 80, .g = 60, .b = 0};
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int j = (i + 1) % 4;
|
||||||
|
if (!vis[i] || !vis[j]) continue;
|
||||||
|
// Clamp to screen to avoid GPU issues
|
||||||
|
int16_t x0 = sx[i], y0 = sy[i], x1 = sx[j], y1 = sy[j];
|
||||||
|
if (x0 < 0) x0 = 0; if (x0 > 319) x0 = 319;
|
||||||
|
if (y0 < 0) y0 = 0; if (y0 > 239) y0 = 239;
|
||||||
|
if (x1 < 0) x1 = 0; if (x1 > 319) x1 = 319;
|
||||||
|
if (y1 < 0) y1 = 0; if (y1 > 239) y1 = 239;
|
||||||
|
|
||||||
|
// Draw line as degenerate triangle (A=B=start, C=end gives a 1px line)
|
||||||
|
auto& tri = balloc.allocateFragment<psyqo::Prim::GouraudTriangle>();
|
||||||
|
tri.primitive.pointA.x = x0; tri.primitive.pointA.y = y0;
|
||||||
|
tri.primitive.pointB.x = x1; tri.primitive.pointB.y = y1;
|
||||||
|
tri.primitive.pointC.x = x1; tri.primitive.pointC.y = (int16_t)(y1 + 1);
|
||||||
|
tri.primitive.setColorA(lineColor);
|
||||||
|
tri.primitive.setColorB(lineColor);
|
||||||
|
tri.primitive.setColorC(lineColor);
|
||||||
|
tri.primitive.setOpaque();
|
||||||
|
ot.insert(tri, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room AABB outlines: project the 8 corners of each room's AABB and draw edges.
|
||||||
|
for (int r = 0; r < roomCount - 1 && r < 8; r++) {
|
||||||
|
bool rendered = (visited & (1u << r)) != 0;
|
||||||
|
psyqo::Color boxColor = rendered ?
|
||||||
|
roomColors[r] : psyqo::Color{.r = 60, .g = 60, .b = 60};
|
||||||
|
|
||||||
|
const RoomData& rm = rooms[r];
|
||||||
|
int32_t bmin[3] = {rm.aabbMinX, rm.aabbMinY, rm.aabbMinZ};
|
||||||
|
int32_t bmax[3] = {rm.aabbMaxX, rm.aabbMaxY, rm.aabbMaxZ};
|
||||||
|
|
||||||
|
// 8 corners of the AABB
|
||||||
|
int16_t csx[8], csy[8];
|
||||||
|
bool cvis[8];
|
||||||
|
int cvisCount = 0;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
int32_t wx = (i & 1) ? bmax[0] : bmin[0];
|
||||||
|
int32_t wy = (i & 2) ? bmax[1] : bmin[1];
|
||||||
|
int32_t wz = (i & 4) ? bmax[2] : bmin[2];
|
||||||
|
int32_t vx, vy, vz;
|
||||||
|
worldToCamera(wx, wy, wz, camX, camY, camZ, camRot, vx, vy, vz);
|
||||||
|
cvis[i] = projectToScreen(vx, vy, vz, csx[i], csy[i]);
|
||||||
|
if (cvis[i]) cvisCount++;
|
||||||
|
}
|
||||||
|
if (cvisCount < 2) continue;
|
||||||
|
|
||||||
|
// Draw 12 AABB edges
|
||||||
|
static const int edges[12][2] = {
|
||||||
|
{0,1},{2,3},{4,5},{6,7}, // X-axis edges
|
||||||
|
{0,2},{1,3},{4,6},{5,7}, // Y-axis edges
|
||||||
|
{0,4},{1,5},{2,6},{3,7}, // Z-axis edges
|
||||||
|
};
|
||||||
|
for (int e = 0; e < 12; e++) {
|
||||||
|
int a = edges[e][0], b = edges[e][1];
|
||||||
|
if (!cvis[a] || !cvis[b]) continue;
|
||||||
|
int16_t x0 = csx[a], y0 = csy[a], x1 = csx[b], y1 = csy[b];
|
||||||
|
if (x0 < 0) x0 = 0; if (x0 > 319) x0 = 319;
|
||||||
|
if (y0 < 0) y0 = 0; if (y0 > 239) y0 = 239;
|
||||||
|
if (x1 < 0) x1 = 0; if (x1 > 319) x1 = 319;
|
||||||
|
if (y1 < 0) y1 = 0; if (y1 > 239) y1 = 239;
|
||||||
|
|
||||||
|
auto& tri = balloc.allocateFragment<psyqo::Prim::GouraudTriangle>();
|
||||||
|
tri.primitive.pointA.x = x0; tri.primitive.pointA.y = y0;
|
||||||
|
tri.primitive.pointB.x = x1; tri.primitive.pointB.y = y1;
|
||||||
|
tri.primitive.pointC.x = x1; tri.primitive.pointC.y = (int16_t)(y1 + 1);
|
||||||
|
tri.primitive.setColorA(boxColor);
|
||||||
|
tri.primitive.setColorB(boxColor);
|
||||||
|
tri.primitive.setColorC(boxColor);
|
||||||
|
tri.primitive.setOpaque();
|
||||||
|
ot.insert(tri, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc);
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
if (m_memOverlay) m_memOverlay->renderOT(ot, balloc);
|
||||||
|
#endif
|
||||||
|
m_gpu.getNextClear(clear.primitive, m_clearcolor);
|
||||||
|
m_gpu.chain(clear); m_gpu.chain(ot);
|
||||||
|
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
if (m_memOverlay) m_memOverlay->renderText(m_gpu);
|
||||||
|
#endif
|
||||||
|
m_frameCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::Renderer::VramUpload(const uint16_t* imageData, int16_t posX,
|
||||||
|
int16_t posY, int16_t width, int16_t height) {
|
||||||
psyqo::Rect uploadRect{.a = {.x = posX, .y = posY}, .b = {width, height}};
|
psyqo::Rect uploadRect{.a = {.x = posX, .y = posY}, .b = {width, height}};
|
||||||
m_gpu.uploadToVRAM(imageData, uploadRect);
|
m_gpu.uploadToVRAM(imageData, uploadRect);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <EASTL/array.h>
|
||||||
#include <EASTL/vector.h>
|
#include <EASTL/vector.h>
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <psyqo/bump-allocator.hh>
|
#include <psyqo/bump-allocator.hh>
|
||||||
#include <psyqo/fragments.hh>
|
#include <psyqo/fragments.hh>
|
||||||
#include <psyqo/gpu.hh>
|
#include <psyqo/gpu.hh>
|
||||||
@@ -10,30 +10,71 @@
|
|||||||
#include <psyqo/ordering-table.hh>
|
#include <psyqo/ordering-table.hh>
|
||||||
#include <psyqo/primitives/common.hh>
|
#include <psyqo/primitives/common.hh>
|
||||||
#include <psyqo/primitives/misc.hh>
|
#include <psyqo/primitives/misc.hh>
|
||||||
|
#include <psyqo/primitives/triangles.hh>
|
||||||
#include <psyqo/trigonometry.hh>
|
#include <psyqo/trigonometry.hh>
|
||||||
|
|
||||||
|
#include "bvh.hh"
|
||||||
#include "camera.hh"
|
#include "camera.hh"
|
||||||
#include "gameobject.hh"
|
#include "gameobject.hh"
|
||||||
|
#include "triclip.hh"
|
||||||
|
|
||||||
namespace psxsplash {
|
namespace psxsplash {
|
||||||
|
|
||||||
|
class UISystem; // Forward declaration
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
class MemOverlay; // Forward declaration
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct FogConfig {
|
||||||
|
bool enabled = false;
|
||||||
|
psyqo::Color color = {.r = 0, .g = 0, .b = 0};
|
||||||
|
uint8_t density = 5;
|
||||||
|
int32_t fogFarSZ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class Renderer final {
|
class Renderer final {
|
||||||
public:
|
public:
|
||||||
Renderer(const Renderer&) = delete;
|
Renderer(const Renderer&) = delete;
|
||||||
Renderer& operator=(const Renderer&) = delete;
|
Renderer& operator=(const Renderer&) = delete;
|
||||||
|
|
||||||
static constexpr size_t ORDERING_TABLE_SIZE = 4096 * 16;
|
#ifndef OT_SIZE
|
||||||
static constexpr size_t BUMP_ALLOCATOR_SIZE = 8192;
|
#define OT_SIZE (2048 * 8)
|
||||||
|
#endif
|
||||||
|
#ifndef BUMP_SIZE
|
||||||
|
#define BUMP_SIZE (8096 * 24)
|
||||||
|
#endif
|
||||||
|
static constexpr size_t ORDERING_TABLE_SIZE = OT_SIZE;
|
||||||
|
static constexpr size_t BUMP_ALLOCATOR_SIZE = BUMP_SIZE;
|
||||||
|
static constexpr size_t MAX_VISIBLE_TRIANGLES = 4096;
|
||||||
|
|
||||||
static void init(psyqo::GPU& gpuInstance);
|
static constexpr int32_t PROJ_H = 120;
|
||||||
|
static constexpr int32_t SCREEN_CX = 160;
|
||||||
|
static constexpr int32_t SCREEN_CY = 120;
|
||||||
|
|
||||||
void setCamera(Camera& camera);
|
static void Init(psyqo::GPU& gpuInstance);
|
||||||
|
void SetCamera(Camera& camera);
|
||||||
|
void SetFog(const FogConfig& fog);
|
||||||
|
|
||||||
void render(eastl::vector<GameObject*>& objects);
|
void Render(eastl::vector<GameObject*>& objects);
|
||||||
void vramUpload(const uint16_t* imageData, int16_t posX, int16_t posY, int16_t width, int16_t height);
|
void RenderWithBVH(eastl::vector<GameObject*>& objects, const BVHManager& bvh);
|
||||||
|
void RenderWithRooms(eastl::vector<GameObject*>& objects,
|
||||||
|
const RoomData* rooms, int roomCount,
|
||||||
|
const PortalData* portals, int portalCount,
|
||||||
|
const TriangleRef* roomTriRefs,
|
||||||
|
int cameraRoom = -1);
|
||||||
|
|
||||||
static Renderer& getInstance() {
|
void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY,
|
||||||
psyqo::Kernel::assert(instance != nullptr, "Access to renderer was tried without prior initialization");
|
int16_t width, int16_t height);
|
||||||
|
|
||||||
|
void SetUISystem(UISystem* ui) { m_uiSystem = ui; }
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
void SetMemOverlay(MemOverlay* overlay) { m_memOverlay = overlay; }
|
||||||
|
#endif
|
||||||
|
psyqo::GPU& getGPU() { return m_gpu; }
|
||||||
|
|
||||||
|
static Renderer& GetInstance() {
|
||||||
|
psyqo::Kernel::assert(instance != nullptr,
|
||||||
|
"Access to renderer was tried without prior initialization");
|
||||||
return *instance;
|
return *instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,15 +84,31 @@ class Renderer final {
|
|||||||
Renderer(psyqo::GPU& gpuInstance) : m_gpu(gpuInstance) {}
|
Renderer(psyqo::GPU& gpuInstance) : m_gpu(gpuInstance) {}
|
||||||
~Renderer() {}
|
~Renderer() {}
|
||||||
|
|
||||||
Camera* m_currentCamera;
|
Camera* m_currentCamera = nullptr;
|
||||||
|
|
||||||
psyqo::GPU& m_gpu;
|
psyqo::GPU& m_gpu;
|
||||||
psyqo::Trig<> m_trig;
|
psyqo::Trig<> m_trig;
|
||||||
|
|
||||||
psyqo::OrderingTable<ORDERING_TABLE_SIZE> m_ots[2];
|
psyqo::OrderingTable<ORDERING_TABLE_SIZE> m_ots[2];
|
||||||
psyqo::Fragments::SimpleFragment<psyqo::Prim::FastFill> m_clear[2];
|
psyqo::Fragments::SimpleFragment<psyqo::Prim::FastFill> m_clear[2];
|
||||||
psyqo::Color m_clearcolor = {.r = 63, .g = 63, .b = 63};
|
|
||||||
psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE> m_ballocs[2];
|
psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE> m_ballocs[2];
|
||||||
|
|
||||||
|
FogConfig m_fog;
|
||||||
|
psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0};
|
||||||
|
|
||||||
|
UISystem* m_uiSystem = nullptr;
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
MemOverlay* m_memOverlay = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TriangleRef m_visibleRefs[MAX_VISIBLE_TRIANGLES];
|
||||||
|
int m_frameCount = 0;
|
||||||
|
|
||||||
|
psyqo::Vec3 computeCameraViewPos();
|
||||||
|
void setupObjectTransform(GameObject* obj, const psyqo::Vec3& cameraPosition);
|
||||||
|
|
||||||
|
void processTriangle(Tri& tri, int32_t fogFarSZ,
|
||||||
|
psyqo::OrderingTable<ORDERING_TABLE_SIZE>& ot,
|
||||||
|
psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE>& balloc);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace psxsplash
|
} // namespace psxsplash
|
||||||
925
src/scenemanager.cpp
Normal file
925
src/scenemanager.cpp
Normal file
@@ -0,0 +1,925 @@
|
|||||||
|
#include "scenemanager.hh"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "collision.hh"
|
||||||
|
#include "profiler.hh"
|
||||||
|
#include "renderer.hh"
|
||||||
|
#include "splashpack.hh"
|
||||||
|
#include "streq.hh"
|
||||||
|
#include "luaapi.hh"
|
||||||
|
#include "loadingscreen.hh"
|
||||||
|
|
||||||
|
#include <psyqo/primitives/misc.hh>
|
||||||
|
#include <psyqo/trigonometry.hh>
|
||||||
|
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
#include "cdromhelper.hh"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
using namespace psyqo::trig_literals;
|
||||||
|
|
||||||
|
using namespace psyqo::fixed_point_literals;
|
||||||
|
|
||||||
|
using namespace psxsplash;
|
||||||
|
|
||||||
|
// Static member definition
|
||||||
|
psyqo::Font<>* psxsplash::SceneManager::s_font = nullptr;
|
||||||
|
|
||||||
|
// Default player collision radius: ~0.5 world units at GTE 100 -> 20 in 20.12
|
||||||
|
static constexpr int32_t PLAYER_RADIUS = 20;
|
||||||
|
|
||||||
|
// Interaction system state
|
||||||
|
static psyqo::Trig<> s_interactTrig;
|
||||||
|
static int s_activePromptCanvas = -1; // Currently shown prompt canvas index (-1 = none)
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingScreen* loading) {
|
||||||
|
auto& gpu = Renderer::GetInstance().getGPU();
|
||||||
|
|
||||||
|
L.Reset();
|
||||||
|
|
||||||
|
// Initialize audio system
|
||||||
|
m_audio.init();
|
||||||
|
|
||||||
|
// Register the Lua API
|
||||||
|
LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer, &m_animationPlayer, &m_uiSystem);
|
||||||
|
|
||||||
|
#ifdef PSXSPLASH_PROFILER
|
||||||
|
debug::Profiler::getInstance().initialize();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SplashpackSceneSetup sceneSetup;
|
||||||
|
m_loader.LoadSplashpack(splashpackData, sceneSetup);
|
||||||
|
|
||||||
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 40);
|
||||||
|
|
||||||
|
m_luaFiles = std::move(sceneSetup.luaFiles);
|
||||||
|
m_gameObjects = std::move(sceneSetup.objects);
|
||||||
|
m_objectNames = std::move(sceneSetup.objectNames);
|
||||||
|
m_bvh = sceneSetup.bvh; // Copy BVH for frustum culling
|
||||||
|
m_navRegions = sceneSetup.navRegions; // Nav region system (v7+)
|
||||||
|
m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION;
|
||||||
|
|
||||||
|
// If nav regions are loaded, camera follows the player. Otherwise the
|
||||||
|
// scene is in "free camera" mode where cutscenes and Lua drive the camera.
|
||||||
|
m_cameraFollowsPlayer = m_navRegions.isLoaded();
|
||||||
|
m_controlsEnabled = true;
|
||||||
|
|
||||||
|
// Scene type and render path
|
||||||
|
m_sceneType = sceneSetup.sceneType;
|
||||||
|
|
||||||
|
// Room/portal data for interior scenes (v11+)
|
||||||
|
m_rooms = sceneSetup.rooms;
|
||||||
|
m_roomCount = sceneSetup.roomCount;
|
||||||
|
m_portals = sceneSetup.portals;
|
||||||
|
m_portalCount = sceneSetup.portalCount;
|
||||||
|
m_roomTriRefs = sceneSetup.roomTriRefs;
|
||||||
|
m_roomTriRefCount = sceneSetup.roomTriRefCount;
|
||||||
|
|
||||||
|
// Configure fog and back color from splashpack data (v11+)
|
||||||
|
{
|
||||||
|
psxsplash::FogConfig fogCfg;
|
||||||
|
fogCfg.enabled = sceneSetup.fogEnabled;
|
||||||
|
fogCfg.color = {.r = sceneSetup.fogR, .g = sceneSetup.fogG, .b = sceneSetup.fogB};
|
||||||
|
fogCfg.density = sceneSetup.fogDensity;
|
||||||
|
Renderer::GetInstance().SetFog(fogCfg);
|
||||||
|
}
|
||||||
|
// Copy component arrays
|
||||||
|
m_interactables = std::move(sceneSetup.interactables);
|
||||||
|
|
||||||
|
// Load audio clips into SPU RAM
|
||||||
|
m_audioClipNames = std::move(sceneSetup.audioClipNames);
|
||||||
|
for (size_t i = 0; i < sceneSetup.audioClips.size(); i++) {
|
||||||
|
auto& clip = sceneSetup.audioClips[i];
|
||||||
|
m_audio.loadClip((int)i, clip.adpcmData, clip.sizeBytes, clip.sampleRate, clip.loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 55);
|
||||||
|
|
||||||
|
// Copy cutscene data into scene manager storage (sceneSetup is stack-local)
|
||||||
|
m_cutsceneCount = sceneSetup.cutsceneCount;
|
||||||
|
for (int i = 0; i < m_cutsceneCount; i++) {
|
||||||
|
m_cutscenes[i] = sceneSetup.loadedCutscenes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize cutscene player (v12+)
|
||||||
|
m_cutscenePlayer.init(
|
||||||
|
m_cutsceneCount > 0 ? m_cutscenes : nullptr,
|
||||||
|
m_cutsceneCount,
|
||||||
|
&m_currentCamera,
|
||||||
|
&m_audio,
|
||||||
|
&m_uiSystem
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy animation data into scene manager storage
|
||||||
|
m_animationCount = sceneSetup.animationCount;
|
||||||
|
for (int i = 0; i < m_animationCount; i++) {
|
||||||
|
m_animations[i] = sceneSetup.loadedAnimations[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize animation player
|
||||||
|
m_animationPlayer.init(
|
||||||
|
m_animationCount > 0 ? m_animations : nullptr,
|
||||||
|
m_animationCount,
|
||||||
|
&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);
|
||||||
|
|
||||||
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 70);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve UI track handles for animation tracks (same logic)
|
||||||
|
for (int ai = 0; ai < m_animationCount; ai++) {
|
||||||
|
for (uint8_t ti = 0; ti < m_animations[ai].trackCount; ti++) {
|
||||||
|
auto& track = m_animations[ai].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;
|
||||||
|
|
||||||
|
if (track.trackType == TrackType::UICanvasVisible) {
|
||||||
|
track.uiHandle = static_cast<int16_t>(m_uiSystem.findCanvas(nameStr));
|
||||||
|
} else {
|
||||||
|
const char* sep = nameStr;
|
||||||
|
while (*sep && *sep != '/') sep++;
|
||||||
|
if (*sep == '/') {
|
||||||
|
char* mutableSep = const_cast<char*>(sep);
|
||||||
|
*mutableSep = '\0';
|
||||||
|
int canvasIdx = m_uiSystem.findCanvas(nameStr);
|
||||||
|
*mutableSep = '/';
|
||||||
|
if (canvasIdx >= 0) {
|
||||||
|
track.uiHandle = static_cast<int16_t>(
|
||||||
|
m_uiSystem.findElement(canvasIdx, sep + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Renderer::GetInstance().SetUISystem(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
if (s_font != nullptr) {
|
||||||
|
m_memOverlay.init(s_font);
|
||||||
|
Renderer::GetInstance().SetMemOverlay(&m_memOverlay);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
m_playerPosition = sceneSetup.playerStartPosition;
|
||||||
|
|
||||||
|
playerRotationX = 0.0_pi;
|
||||||
|
playerRotationY = 0.0_pi;
|
||||||
|
playerRotationZ = 0.0_pi;
|
||||||
|
|
||||||
|
m_playerHeight = sceneSetup.playerHeight;
|
||||||
|
|
||||||
|
m_controls.setMoveSpeed(sceneSetup.moveSpeed);
|
||||||
|
m_controls.setSprintSpeed(sceneSetup.sprintSpeed);
|
||||||
|
m_playerRadius = (int32_t)sceneSetup.playerRadius.value;
|
||||||
|
if (m_playerRadius == 0) m_playerRadius = PLAYER_RADIUS;
|
||||||
|
m_jumpVelocityRaw = (int32_t)sceneSetup.jumpVelocity.value;
|
||||||
|
int32_t gravityRaw = (int32_t)sceneSetup.gravity.value;
|
||||||
|
m_gravityPerFrame = gravityRaw / 30;
|
||||||
|
if (m_gravityPerFrame == 0 && gravityRaw > 0) m_gravityPerFrame = 1;
|
||||||
|
m_velocityY = 0;
|
||||||
|
m_isGrounded = true;
|
||||||
|
m_lastFrameTime = 0;
|
||||||
|
m_deltaFrames = 1;
|
||||||
|
|
||||||
|
m_collisionSystem.init();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sceneSetup.colliders.size(); i++) {
|
||||||
|
SPLASHPACKCollider* collider = sceneSetup.colliders[i];
|
||||||
|
if (collider == nullptr) continue;
|
||||||
|
|
||||||
|
AABB bounds;
|
||||||
|
bounds.min.x.value = collider->minX;
|
||||||
|
bounds.min.y.value = collider->minY;
|
||||||
|
bounds.min.z.value = collider->minZ;
|
||||||
|
bounds.max.x.value = collider->maxX;
|
||||||
|
bounds.max.y.value = collider->maxY;
|
||||||
|
bounds.max.z.value = collider->maxZ;
|
||||||
|
|
||||||
|
CollisionType type = static_cast<CollisionType>(collider->collisionType);
|
||||||
|
|
||||||
|
m_collisionSystem.registerCollider(
|
||||||
|
collider->gameObjectIndex,
|
||||||
|
bounds,
|
||||||
|
type,
|
||||||
|
collider->layerMask
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sceneSetup.triggerBoxes.size(); i++) {
|
||||||
|
SPLASHPACKTriggerBox* tb = sceneSetup.triggerBoxes[i];
|
||||||
|
if (tb == nullptr) continue;
|
||||||
|
|
||||||
|
AABB bounds;
|
||||||
|
bounds.min.x.value = tb->minX;
|
||||||
|
bounds.min.y.value = tb->minY;
|
||||||
|
bounds.min.z.value = tb->minZ;
|
||||||
|
bounds.max.x.value = tb->maxX;
|
||||||
|
bounds.max.y.value = tb->maxY;
|
||||||
|
bounds.max.z.value = tb->maxZ;
|
||||||
|
|
||||||
|
m_collisionSystem.registerTriggerBox(bounds, tb->luaFileIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < m_luaFiles.size(); i++) {
|
||||||
|
auto luaFile = m_luaFiles[i];
|
||||||
|
L.LoadLuaFile(luaFile->luaCode, luaFile->length, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 85);
|
||||||
|
|
||||||
|
L.RegisterSceneScripts(sceneSetup.sceneLuaFileIndex);
|
||||||
|
|
||||||
|
L.OnSceneCreationStart();
|
||||||
|
|
||||||
|
for (auto object : m_gameObjects) {
|
||||||
|
L.RegisterGameObject(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire all onCreate events AFTER all objects are registered,
|
||||||
|
// so Entity.Find works across all objects in onCreate handlers.
|
||||||
|
if (!m_gameObjects.empty()) {
|
||||||
|
L.FireAllOnCreate(
|
||||||
|
reinterpret_cast<GameObject**>(m_gameObjects.data()),
|
||||||
|
m_gameObjects.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_controls.forceAnalogMode();
|
||||||
|
m_controls.Init();
|
||||||
|
Renderer::GetInstance().SetCamera(m_currentCamera);
|
||||||
|
|
||||||
|
L.OnSceneCreationEnd();
|
||||||
|
|
||||||
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 95);
|
||||||
|
|
||||||
|
m_liveDataSize = sceneSetup.liveDataSize;
|
||||||
|
shrinkBuffer();
|
||||||
|
|
||||||
|
if (loading && loading->isActive()) loading->updateProgress(gpu, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
|
||||||
|
LuaAPI::IncrementFrameCount();
|
||||||
|
|
||||||
|
m_cutscenePlayer.tick();
|
||||||
|
m_animationPlayer.tick();
|
||||||
|
|
||||||
|
{
|
||||||
|
uint32_t now = gpu.now();
|
||||||
|
if (m_lastFrameTime != 0) {
|
||||||
|
uint32_t elapsed = now - m_lastFrameTime;
|
||||||
|
m_deltaFrames = (elapsed > 50000) ? 2 : 1;
|
||||||
|
if (elapsed > 83000) m_deltaFrames = 3;
|
||||||
|
}
|
||||||
|
m_lastFrameTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t renderingStart = gpu.now();
|
||||||
|
auto& renderer = psxsplash::Renderer::GetInstance();
|
||||||
|
if (m_sceneType == 1 && m_roomCount > 0 && m_rooms != nullptr) {
|
||||||
|
|
||||||
|
int camRoom = -1;
|
||||||
|
if (m_navRegions.isLoaded()) {
|
||||||
|
if (m_cutscenePlayer.isPlaying() && m_cutscenePlayer.hasCameraTracks()) {
|
||||||
|
auto& camPos = m_currentCamera.GetPosition();
|
||||||
|
uint16_t camRegion = m_navRegions.findRegion(camPos.x.value, camPos.z.value);
|
||||||
|
if (camRegion != NAV_NO_REGION) {
|
||||||
|
uint8_t ri = m_navRegions.getRoomIndex(camRegion);
|
||||||
|
if (ri != 0xFF) camRoom = (int)ri;
|
||||||
|
}
|
||||||
|
} else if (m_playerNavRegion != NAV_NO_REGION) {
|
||||||
|
uint8_t ri = m_navRegions.getRoomIndex(m_playerNavRegion);
|
||||||
|
if (ri != 0xFF) camRoom = (int)ri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderer.RenderWithRooms(m_gameObjects, m_rooms, m_roomCount,
|
||||||
|
m_portals, m_portalCount, m_roomTriRefs, camRoom);
|
||||||
|
} else {
|
||||||
|
renderer.RenderWithBVH(m_gameObjects, m_bvh);
|
||||||
|
}
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
uint32_t renderingEnd = gpu.now();
|
||||||
|
uint32_t renderingTime = renderingEnd - renderingStart;
|
||||||
|
|
||||||
|
#ifdef PSXSPLASH_PROFILER
|
||||||
|
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_RENDERING, renderingTime);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint32_t collisionStart = gpu.now();
|
||||||
|
|
||||||
|
AABB playerAABB;
|
||||||
|
{
|
||||||
|
psyqo::FixedPoint<12> r;
|
||||||
|
r.value = m_playerRadius;
|
||||||
|
psyqo::FixedPoint<12> px = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x);
|
||||||
|
psyqo::FixedPoint<12> py = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y);
|
||||||
|
psyqo::FixedPoint<12> pz = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z);
|
||||||
|
psyqo::FixedPoint<12> h = static_cast<psyqo::FixedPoint<12>>(m_playerHeight);
|
||||||
|
// Y is inverted on PS1: negative = up, positive = down.
|
||||||
|
// m_playerPosition.y is the camera (head), feet are at py + h.
|
||||||
|
// Leave a small gap at the bottom so the floor geometry doesn't
|
||||||
|
// trigger constant collisions (floor contact is handled by nav).
|
||||||
|
psyqo::FixedPoint<12> bodyBottom;
|
||||||
|
bodyBottom.value = h.value * 3 / 4; // 75% of height below camera
|
||||||
|
playerAABB.min = psyqo::Vec3{px - r, py, pz - r};
|
||||||
|
playerAABB.max = psyqo::Vec3{px + r, py + bodyBottom, pz + r};
|
||||||
|
}
|
||||||
|
|
||||||
|
psyqo::Vec3 pushBack;
|
||||||
|
int collisionCount = m_collisionSystem.detectCollisions(playerAABB, pushBack, *this);
|
||||||
|
|
||||||
|
{
|
||||||
|
psyqo::FixedPoint<12> zero;
|
||||||
|
if (pushBack.x != zero || pushBack.z != zero) {
|
||||||
|
m_playerPosition.x = m_playerPosition.x + pushBack.x;
|
||||||
|
m_playerPosition.z = m_playerPosition.z + pushBack.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire onCollideWithPlayer Lua events on collided objects
|
||||||
|
const CollisionResult* results = m_collisionSystem.getResults();
|
||||||
|
for (int i = 0; i < collisionCount; i++) {
|
||||||
|
if (results[i].objectA != 0xFFFF) continue;
|
||||||
|
auto* obj = getGameObject(results[i].objectB);
|
||||||
|
if (obj) {
|
||||||
|
L.OnCollideWithPlayer(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process trigger boxes (enter/exit)
|
||||||
|
m_collisionSystem.detectTriggers(playerAABB, *this);
|
||||||
|
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
uint32_t collisionEnd = gpu.now();
|
||||||
|
|
||||||
|
uint32_t luaStart = gpu.now();
|
||||||
|
// Lua update tick - call onUpdate for all registered objects with onUpdate handler
|
||||||
|
for (auto* go : m_gameObjects) {
|
||||||
|
if (go && go->isActive()) {
|
||||||
|
L.OnUpdate(go, m_deltaFrames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
uint32_t luaEnd = gpu.now();
|
||||||
|
uint32_t luaTime = luaEnd - luaStart;
|
||||||
|
#ifdef PSXSPLASH_PROFILER
|
||||||
|
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_LUA, luaTime);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Update game systems
|
||||||
|
processEnableDisableEvents();
|
||||||
|
|
||||||
|
|
||||||
|
uint32_t controlsStart = gpu.now();
|
||||||
|
|
||||||
|
// Update button state tracking first
|
||||||
|
m_controls.UpdateButtonStates();
|
||||||
|
|
||||||
|
// Update interaction system (checks for interact button press)
|
||||||
|
updateInteractionSystem();
|
||||||
|
|
||||||
|
// Dispatch button events to all objects
|
||||||
|
uint16_t pressed = m_controls.getButtonsPressed();
|
||||||
|
uint16_t released = m_controls.getButtonsReleased();
|
||||||
|
|
||||||
|
if (pressed || released) {
|
||||||
|
// Only iterate objects if there are button events
|
||||||
|
for (auto* go : m_gameObjects) {
|
||||||
|
if (!go || !go->isActive()) continue;
|
||||||
|
|
||||||
|
if (pressed) {
|
||||||
|
// Dispatch press events for each pressed button
|
||||||
|
for (int btn = 0; btn < 16; btn++) {
|
||||||
|
if (pressed & (1 << btn)) {
|
||||||
|
L.OnButtonPress(go, btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (released) {
|
||||||
|
// Dispatch release events for each released button
|
||||||
|
for (int btn = 0; btn < 16; btn++) {
|
||||||
|
if (released & (1 << btn)) {
|
||||||
|
L.OnButtonRelease(go, btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save position BEFORE movement for collision detection
|
||||||
|
psyqo::Vec3 oldPlayerPosition = m_playerPosition;
|
||||||
|
|
||||||
|
if (m_controlsEnabled) {
|
||||||
|
m_controls.HandleControls(m_playerPosition, playerRotationX, playerRotationY, playerRotationZ, freecam, m_deltaFrames);
|
||||||
|
|
||||||
|
// Jump input: Cross button triggers jump when grounded
|
||||||
|
if (m_isGrounded && m_controls.wasButtonPressed(psyqo::AdvancedPad::Button::Cross)) {
|
||||||
|
m_velocityY = -m_jumpVelocityRaw; // Negative = upward (PSX Y-down)
|
||||||
|
m_isGrounded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
uint32_t controlsEnd = gpu.now();
|
||||||
|
uint32_t controlsTime = controlsEnd - controlsStart;
|
||||||
|
#ifdef PSXSPLASH_PROFILER
|
||||||
|
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_CONTROLS, controlsTime);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint32_t navmeshStart = gpu.now();
|
||||||
|
if (!freecam && m_navRegions.isLoaded()) {
|
||||||
|
// Apply gravity
|
||||||
|
for (int f = 0; f < m_deltaFrames; f++) {
|
||||||
|
m_velocityY += m_gravityPerFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply vertical velocity to position
|
||||||
|
int32_t posYDelta = (m_velocityY * m_deltaFrames) / 30;
|
||||||
|
m_playerPosition.y.value += posYDelta;
|
||||||
|
|
||||||
|
// Resolve position via nav regions
|
||||||
|
uint16_t prevRegion = m_playerNavRegion;
|
||||||
|
int32_t px = m_playerPosition.x.value;
|
||||||
|
int32_t pz = m_playerPosition.z.value;
|
||||||
|
int32_t floorY = m_navRegions.resolvePosition(
|
||||||
|
px, pz, m_playerNavRegion);
|
||||||
|
|
||||||
|
if (m_playerNavRegion != NAV_NO_REGION) {
|
||||||
|
m_playerPosition.x.value = px;
|
||||||
|
m_playerPosition.z.value = pz;
|
||||||
|
|
||||||
|
int32_t cameraAtFloor = floorY - m_playerHeight.raw();
|
||||||
|
|
||||||
|
if (m_playerPosition.y.value >= cameraAtFloor) {
|
||||||
|
m_playerPosition.y.value = cameraAtFloor;
|
||||||
|
m_velocityY = 0;
|
||||||
|
m_isGrounded = true;
|
||||||
|
} else {
|
||||||
|
m_isGrounded = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m_playerPosition = oldPlayerPosition;
|
||||||
|
m_playerNavRegion = prevRegion;
|
||||||
|
m_velocityY = 0;
|
||||||
|
m_isGrounded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
uint32_t navmeshEnd = gpu.now();
|
||||||
|
uint32_t navmeshTime = navmeshEnd - navmeshStart;
|
||||||
|
#ifdef PSXSPLASH_PROFILER
|
||||||
|
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_NAVMESH, navmeshTime);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Only snap camera to player when in player-follow mode and no
|
||||||
|
// cutscene is actively controlling the camera. In free camera mode
|
||||||
|
// (no nav regions / no PSXPlayer), the camera is driven entirely
|
||||||
|
// by cutscenes and Lua. After a cutscene ends in free mode, the
|
||||||
|
// camera stays at the last cutscene position.
|
||||||
|
if (m_cameraFollowsPlayer && !(m_cutscenePlayer.isPlaying() && m_cutscenePlayer.hasCameraTracks())) {
|
||||||
|
m_currentCamera.SetPosition(static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x),
|
||||||
|
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y),
|
||||||
|
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z));
|
||||||
|
m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process pending scene transitions (at end of frame)
|
||||||
|
processPendingSceneLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::fireTriggerEnter(int16_t luaFileIndex, uint16_t triggerIndex) {
|
||||||
|
if (luaFileIndex < 0) return;
|
||||||
|
L.OnTriggerEnterScript(luaFileIndex, triggerIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::fireTriggerExit(int16_t luaFileIndex, uint16_t triggerIndex) {
|
||||||
|
if (luaFileIndex < 0) return;
|
||||||
|
L.OnTriggerExitScript(luaFileIndex, triggerIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// INTERACTION SYSTEM
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::updateInteractionSystem() {
|
||||||
|
// Tick cooldowns for all interactables
|
||||||
|
for (auto* interactable : m_interactables) {
|
||||||
|
if (interactable) interactable->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player position for distance checks
|
||||||
|
psyqo::FixedPoint<12> playerX = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x);
|
||||||
|
psyqo::FixedPoint<12> playerY = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y);
|
||||||
|
psyqo::FixedPoint<12> playerZ = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z);
|
||||||
|
|
||||||
|
// Player forward direction from Y rotation (for line-of-sight checks)
|
||||||
|
psyqo::FixedPoint<12> forwardX = s_interactTrig.sin(playerRotationY);
|
||||||
|
psyqo::FixedPoint<12> forwardZ = s_interactTrig.cos(playerRotationY);
|
||||||
|
|
||||||
|
// First pass: find which interactable is closest and in range (for prompt display)
|
||||||
|
Interactable* inRange = nullptr;
|
||||||
|
psyqo::FixedPoint<12> closestDistSq;
|
||||||
|
closestDistSq.value = 0x7FFFFFFF;
|
||||||
|
|
||||||
|
for (auto* interactable : m_interactables) {
|
||||||
|
if (!interactable) continue;
|
||||||
|
if (interactable->isDisabled()) continue;
|
||||||
|
|
||||||
|
auto* go = getGameObject(interactable->gameObjectIndex);
|
||||||
|
if (!go || !go->isActive()) continue;
|
||||||
|
|
||||||
|
// Distance check
|
||||||
|
psyqo::FixedPoint<12> dx = playerX - go->position.x;
|
||||||
|
psyqo::FixedPoint<12> dy = playerY - go->position.y;
|
||||||
|
psyqo::FixedPoint<12> dz = playerZ - go->position.z;
|
||||||
|
psyqo::FixedPoint<12> distSq = dx * dx + dy * dy + dz * dz;
|
||||||
|
|
||||||
|
if (distSq > interactable->radiusSquared) continue;
|
||||||
|
|
||||||
|
// Line-of-sight check: dot product of forward vector and direction to object
|
||||||
|
if (interactable->requireLineOfSight()) {
|
||||||
|
// dot = forwardX * dx + forwardZ * dz (XZ plane only)
|
||||||
|
// Negative dot means object is behind the player
|
||||||
|
psyqo::FixedPoint<12> dot = forwardX * dx + forwardZ * dz;
|
||||||
|
// Object must be in front of the player (dot < 0 in the coordinate system
|
||||||
|
// because dx points FROM player TO object, and forward points where player faces)
|
||||||
|
// Actually: dx = playerX - objX, so it points FROM object TO player.
|
||||||
|
// We want the object in front, so we need -dx direction to align with forward.
|
||||||
|
// dot(forward, objDir) where objDir = obj - player = -dx, -dz
|
||||||
|
psyqo::FixedPoint<12> facingDot = -(forwardX * dx + forwardZ * dz);
|
||||||
|
if (facingDot.value <= 0) continue; // Object is behind the player
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distSq < closestDistSq) {
|
||||||
|
inRange = interactable;
|
||||||
|
closestDistSq = distSq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt canvas management: show only when in range AND can interact
|
||||||
|
int newPromptCanvas = -1;
|
||||||
|
if (inRange && inRange->canInteract() && inRange->showPrompt() && inRange->promptCanvasName[0] != '\0') {
|
||||||
|
newPromptCanvas = m_uiSystem.findCanvas(inRange->promptCanvasName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPromptCanvas != s_activePromptCanvas) {
|
||||||
|
// Hide old prompt
|
||||||
|
if (s_activePromptCanvas >= 0) {
|
||||||
|
m_uiSystem.setCanvasVisible(s_activePromptCanvas, false);
|
||||||
|
}
|
||||||
|
// Show new prompt
|
||||||
|
if (newPromptCanvas >= 0) {
|
||||||
|
m_uiSystem.setCanvasVisible(newPromptCanvas, true);
|
||||||
|
}
|
||||||
|
s_activePromptCanvas = newPromptCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the closest in-range interactable can be activated
|
||||||
|
if (!inRange || !inRange->canInteract()) return;
|
||||||
|
|
||||||
|
// Check if the correct button for this interactable was pressed
|
||||||
|
auto button = static_cast<psyqo::AdvancedPad::Button>(
|
||||||
|
static_cast<uint16_t>(inRange->interactButton));
|
||||||
|
if (!m_controls.wasButtonPressed(button)) return;
|
||||||
|
|
||||||
|
// Trigger the interaction
|
||||||
|
triggerInteraction(getGameObject(inRange->gameObjectIndex));
|
||||||
|
inRange->triggerCooldown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::triggerInteraction(GameObject* interactable) {
|
||||||
|
if (!interactable) return;
|
||||||
|
L.OnInteract(interactable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ENABLE/DISABLE SYSTEM
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::setObjectActive(GameObject* go, bool active) {
|
||||||
|
if (!go) return;
|
||||||
|
|
||||||
|
bool wasActive = go->isActive();
|
||||||
|
if (wasActive == active) return; // No change
|
||||||
|
|
||||||
|
go->setActive(active);
|
||||||
|
|
||||||
|
// Fire appropriate event
|
||||||
|
if (active) {
|
||||||
|
L.OnEnable(go);
|
||||||
|
} else {
|
||||||
|
L.OnDisable(go);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::processEnableDisableEvents() {
|
||||||
|
// Process any pending enable/disable flags.
|
||||||
|
// Uses raw bit manipulation on flagsAsInt instead of the BitField
|
||||||
|
// accessors to avoid a known issue where the BitSpan get/set
|
||||||
|
// operations don't behave correctly on the MIPS target.
|
||||||
|
for (auto* go : m_gameObjects) {
|
||||||
|
if (!go) continue;
|
||||||
|
|
||||||
|
// Bit 1 = pendingEnable
|
||||||
|
if (go->flagsAsInt & 0x02) {
|
||||||
|
go->flagsAsInt &= ~0x02u; // clear pending
|
||||||
|
if (!(go->flagsAsInt & 0x01)) { // if not already active
|
||||||
|
go->flagsAsInt |= 0x01; // set active
|
||||||
|
L.OnEnable(go);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bit 2 = pendingDisable
|
||||||
|
if (go->flagsAsInt & 0x04) {
|
||||||
|
go->flagsAsInt &= ~0x04u; // clear pending
|
||||||
|
if (go->flagsAsInt & 0x01) { // if currently active
|
||||||
|
go->flagsAsInt &= ~0x01u; // clear active
|
||||||
|
L.OnDisable(go);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SCENE LOADING
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::requestSceneLoad(int sceneIndex) {
|
||||||
|
m_pendingSceneIndex = sceneIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::processPendingSceneLoad() {
|
||||||
|
if (m_pendingSceneIndex < 0) return;
|
||||||
|
|
||||||
|
int targetIndex = m_pendingSceneIndex;
|
||||||
|
m_pendingSceneIndex = -1;
|
||||||
|
|
||||||
|
auto& gpu = Renderer::GetInstance().getGPU();
|
||||||
|
loadScene(gpu, targetIndex, /*isFirstScene=*/false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::loadScene(psyqo::GPU& gpu, int sceneIndex, bool isFirstScene) {
|
||||||
|
// Restore CD-ROM controller and CPU IRQ state for file loading.
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
CDRomHelper::WakeDrive();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Build filename using the active backend's naming convention
|
||||||
|
char filename[32];
|
||||||
|
FileLoader::BuildSceneFilename(sceneIndex, filename, sizeof(filename));
|
||||||
|
|
||||||
|
psyqo::Prim::FastFill ff(psyqo::Color{.r = 0, .g = 0, .b = 0});
|
||||||
|
ff.rect = psyqo::Rect{0, 0, 320, 240};
|
||||||
|
gpu.sendPrimitive(ff);
|
||||||
|
ff.rect = psyqo::Rect{0, 256, 320, 240};
|
||||||
|
gpu.sendPrimitive(ff);
|
||||||
|
gpu.pumpCallbacks();
|
||||||
|
|
||||||
|
LoadingScreen loading;
|
||||||
|
if (s_font) {
|
||||||
|
if (loading.load(gpu, *s_font, sceneIndex)) {
|
||||||
|
loading.renderInitialAndFree(gpu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFirstScene) {
|
||||||
|
// Tear down EVERYTHING in the current scene first —
|
||||||
|
// Lua VM, vector backing storage, audio. This returns as much
|
||||||
|
// heap memory as possible before any new allocation.
|
||||||
|
clearScene();
|
||||||
|
|
||||||
|
// Free old splashpack data BEFORE loading the new one.
|
||||||
|
// This avoids having both scene buffers in the heap simultaneously.
|
||||||
|
if (m_currentSceneData) {
|
||||||
|
FileLoader::Get().FreeFile(m_currentSceneData);
|
||||||
|
m_currentSceneData = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading.isActive()) loading.updateProgress(gpu, 20);
|
||||||
|
|
||||||
|
// Load scene data — use progress-aware variant so the loading bar
|
||||||
|
// animates during the (potentially slow) CD-ROM read.
|
||||||
|
int fileSize = 0;
|
||||||
|
uint8_t* newData = nullptr;
|
||||||
|
|
||||||
|
if (loading.isActive()) {
|
||||||
|
struct Ctx { LoadingScreen* ls; psyqo::GPU* gpu; };
|
||||||
|
Ctx ctx{&loading, &gpu};
|
||||||
|
FileLoader::LoadProgressInfo progress{
|
||||||
|
[](uint8_t pct, void* ud) {
|
||||||
|
auto* c = static_cast<Ctx*>(ud);
|
||||||
|
c->ls->updateProgress(*c->gpu, pct);
|
||||||
|
},
|
||||||
|
&ctx, 20, 30
|
||||||
|
};
|
||||||
|
newData = FileLoader::Get().LoadFileSyncWithProgress(
|
||||||
|
filename, fileSize, &progress);
|
||||||
|
} else {
|
||||||
|
newData = FileLoader::Get().LoadFileSync(filename, fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newData && isFirstScene) {
|
||||||
|
// Fallback: try legacy name for backwards compatibility (PCdrv only)
|
||||||
|
newData = FileLoader::Get().LoadFileSync("output.bin", fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading.isActive()) loading.updateProgress(gpu, 30);
|
||||||
|
|
||||||
|
// Stop the CD-ROM motor and mask all interrupts for gameplay.
|
||||||
|
#if defined(LOADER_CDROM)
|
||||||
|
CDRomHelper::SilenceDrive();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
m_currentSceneData = newData;
|
||||||
|
m_currentSceneIndex = sceneIndex;
|
||||||
|
|
||||||
|
// Initialize with new data (creates fresh Lua VM inside)
|
||||||
|
InitializeScene(newData, loading.isActive() ? &loading : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::clearScene() {
|
||||||
|
// 1. Shut down the Lua VM first — frees ALL Lua-allocated memory
|
||||||
|
// (bytecode, strings, tables, registry) in one shot via lua_close.
|
||||||
|
L.Shutdown();
|
||||||
|
|
||||||
|
// 2. Clear all vectors to free their heap storage (game objects, Lua files, names, etc)
|
||||||
|
{ eastl::vector<GameObject*> tmp; tmp.swap(m_gameObjects); }
|
||||||
|
{ eastl::vector<LuaFile*> tmp; tmp.swap(m_luaFiles); }
|
||||||
|
{ eastl::vector<const char*> tmp; tmp.swap(m_objectNames); }
|
||||||
|
{ eastl::vector<const char*> tmp; tmp.swap(m_audioClipNames); }
|
||||||
|
{ eastl::vector<Interactable*> tmp; tmp.swap(m_interactables); }
|
||||||
|
|
||||||
|
// 3. Reset hardware / subsystems
|
||||||
|
m_audio.reset(); // Free SPU RAM and stop all voices
|
||||||
|
m_collisionSystem.init(); // Re-init collision system
|
||||||
|
m_cutsceneCount = 0;
|
||||||
|
s_activePromptCanvas = -1; // Reset prompt tracking
|
||||||
|
m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player
|
||||||
|
m_animationCount = 0;
|
||||||
|
m_animationPlayer.init(nullptr, 0); // Reset animation player
|
||||||
|
// BVH 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;
|
||||||
|
m_portals = nullptr;
|
||||||
|
m_portalCount = 0;
|
||||||
|
m_roomTriRefs = nullptr;
|
||||||
|
m_roomTriRefCount = 0;
|
||||||
|
m_sceneType = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void psxsplash::SceneManager::shrinkBuffer() {
|
||||||
|
if (m_liveDataSize == 0 || m_currentSceneData == nullptr) return;
|
||||||
|
|
||||||
|
uint8_t* oldBase = m_currentSceneData;
|
||||||
|
|
||||||
|
uint8_t* volatile newBaseV = new uint8_t[m_liveDataSize];
|
||||||
|
uint8_t* newBase = newBaseV;
|
||||||
|
if (!newBase) return;
|
||||||
|
__builtin_memcpy(newBase, oldBase, m_liveDataSize);
|
||||||
|
|
||||||
|
intptr_t delta = reinterpret_cast<intptr_t>(newBase) - reinterpret_cast<intptr_t>(oldBase);
|
||||||
|
|
||||||
|
auto reloc = [delta](auto* ptr) -> decltype(ptr) {
|
||||||
|
if (!ptr) return ptr;
|
||||||
|
return reinterpret_cast<decltype(ptr)>(reinterpret_cast<intptr_t>(ptr) + delta);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto& go : m_gameObjects) {
|
||||||
|
go = reloc(go);
|
||||||
|
go->polygons = reloc(go->polygons);
|
||||||
|
}
|
||||||
|
for (auto& lf : m_luaFiles) {
|
||||||
|
lf = reloc(lf);
|
||||||
|
lf->luaCode = reloc(lf->luaCode);
|
||||||
|
}
|
||||||
|
for (auto& name : m_objectNames) name = reloc(name);
|
||||||
|
for (auto& name : m_audioClipNames) name = reloc(name);
|
||||||
|
for (auto& inter : m_interactables) inter = reloc(inter);
|
||||||
|
|
||||||
|
m_bvh.relocate(delta);
|
||||||
|
m_navRegions.relocate(delta);
|
||||||
|
|
||||||
|
m_rooms = reloc(m_rooms);
|
||||||
|
m_portals = reloc(m_portals);
|
||||||
|
m_roomTriRefs = reloc(m_roomTriRefs);
|
||||||
|
|
||||||
|
for (int ci = 0; ci < m_cutsceneCount; ci++) {
|
||||||
|
auto& cs = m_cutscenes[ci];
|
||||||
|
cs.name = reloc(cs.name);
|
||||||
|
cs.audioEvents = reloc(cs.audioEvents);
|
||||||
|
for (uint8_t ti = 0; ti < cs.trackCount; ti++) {
|
||||||
|
auto& track = cs.tracks[ti];
|
||||||
|
track.keyframes = reloc(track.keyframes);
|
||||||
|
if (track.target) track.target = reloc(track.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int ai = 0; ai < m_animationCount; ai++) {
|
||||||
|
auto& an = m_animations[ai];
|
||||||
|
an.name = reloc(an.name);
|
||||||
|
for (uint8_t ti = 0; ti < an.trackCount; ti++) {
|
||||||
|
auto& track = an.tracks[ti];
|
||||||
|
track.keyframes = reloc(track.keyframes);
|
||||||
|
if (track.target) track.target = reloc(track.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_uiSystem.relocate(delta);
|
||||||
|
|
||||||
|
if (!m_gameObjects.empty()) {
|
||||||
|
L.RelocateGameObjects(
|
||||||
|
reinterpret_cast<GameObject**>(m_gameObjects.data()),
|
||||||
|
m_gameObjects.size(), delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileLoader::Get().FreeFile(oldBase);
|
||||||
|
m_currentSceneData = newBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// OBJECT NAME LOOKUP
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
psxsplash::GameObject* psxsplash::SceneManager::findObjectByName(const char* name) const {
|
||||||
|
if (!name || m_objectNames.empty()) return nullptr;
|
||||||
|
for (size_t i = 0; i < m_objectNames.size() && i < m_gameObjects.size(); i++) {
|
||||||
|
if (m_objectNames[i] && streq(m_objectNames[i], name)) {
|
||||||
|
return m_gameObjects[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int psxsplash::SceneManager::findAudioClipByName(const char* name) const {
|
||||||
|
if (!name || m_audioClipNames.empty()) return -1;
|
||||||
|
for (size_t i = 0; i < m_audioClipNames.size(); i++) {
|
||||||
|
if (m_audioClipNames[i] && streq(m_audioClipNames[i], name)) {
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
198
src/scenemanager.hh
Normal file
198
src/scenemanager.hh
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <EASTL/vector.h>
|
||||||
|
|
||||||
|
#include <psyqo/trigonometry.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
#include <psyqo/gpu.hh>
|
||||||
|
|
||||||
|
#include "bvh.hh"
|
||||||
|
#include "camera.hh"
|
||||||
|
#include "collision.hh"
|
||||||
|
#include "controls.hh"
|
||||||
|
#include "gameobject.hh"
|
||||||
|
#include "lua.h"
|
||||||
|
#include "splashpack.hh"
|
||||||
|
#include "navregion.hh"
|
||||||
|
#include "audiomanager.hh"
|
||||||
|
#include "interactable.hh"
|
||||||
|
#include "luaapi.hh"
|
||||||
|
#include "fileloader.hh"
|
||||||
|
#include "cutscene.hh"
|
||||||
|
#include "animation.hh"
|
||||||
|
#include "uisystem.hh"
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
#include "memoverlay.hh"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// Forward-declare; full definition in loadingscreen.hh
|
||||||
|
class LoadingScreen;
|
||||||
|
|
||||||
|
class SceneManager {
|
||||||
|
public:
|
||||||
|
void InitializeScene(uint8_t* splashpackData, LoadingScreen* loading = nullptr);
|
||||||
|
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 for trigger boxes)
|
||||||
|
void fireTriggerEnter(int16_t luaFileIndex, uint16_t triggerIndex);
|
||||||
|
void fireTriggerExit(int16_t luaFileIndex, uint16_t triggerIndex);
|
||||||
|
|
||||||
|
// Get game object by index (for collision callbacks)
|
||||||
|
GameObject* getGameObject(uint16_t index) {
|
||||||
|
if (index < m_gameObjects.size()) return m_gameObjects[index];
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total object count
|
||||||
|
size_t getGameObjectCount() const { return m_gameObjects.size(); }
|
||||||
|
|
||||||
|
// Get object name by index (returns nullptr if no name table or out of range)
|
||||||
|
const char* getObjectName(uint16_t index) const {
|
||||||
|
if (index < m_objectNames.size()) return m_objectNames[index];
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find first object with matching name (linear scan, case-sensitive)
|
||||||
|
GameObject* findObjectByName(const char* name) const;
|
||||||
|
|
||||||
|
// Find audio clip index by name (returns -1 if not found)
|
||||||
|
int findAudioClipByName(const char* name) const;
|
||||||
|
|
||||||
|
// Get audio clip name by index (returns nullptr if out of range)
|
||||||
|
const char* getAudioClipName(int index) const {
|
||||||
|
if (index >= 0 && index < (int)m_audioClipNames.size()) return m_audioClipNames[index];
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API for game systems
|
||||||
|
// Interaction system - call from Lua or native code
|
||||||
|
void triggerInteraction(GameObject* interactable);
|
||||||
|
|
||||||
|
// GameObject state control with events
|
||||||
|
void setObjectActive(GameObject* go, bool active);
|
||||||
|
|
||||||
|
// Public accessors for Lua API
|
||||||
|
Controls& getControls() { return m_controls; }
|
||||||
|
Camera& getCamera() { return m_currentCamera; }
|
||||||
|
Lua& getLua() { return L; }
|
||||||
|
AudioManager& getAudio() { return m_audio; }
|
||||||
|
|
||||||
|
// Controls enable/disable (Lua-driven)
|
||||||
|
void setControlsEnabled(bool enabled) { m_controlsEnabled = enabled; }
|
||||||
|
bool isControlsEnabled() const { return m_controlsEnabled; }
|
||||||
|
|
||||||
|
// Interactable access (for Lua API)
|
||||||
|
Interactable* getInteractable(uint16_t index) {
|
||||||
|
if (index < m_interactables.size()) return m_interactables[index];
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scene loading (for multi-scene support)
|
||||||
|
void requestSceneLoad(int sceneIndex);
|
||||||
|
int getCurrentSceneIndex() const { return m_currentSceneIndex; }
|
||||||
|
|
||||||
|
/// Load a scene by index. This is the ONE canonical load path used by
|
||||||
|
/// both the initial boot (main.cpp) and runtime scene transitions.
|
||||||
|
/// Blanks the screen, shows a loading screen, tears down the old scene,
|
||||||
|
/// loads the new splashpack, and initialises.
|
||||||
|
/// @param gpu GPU reference.
|
||||||
|
/// @param sceneIndex Scene to load.
|
||||||
|
/// @param isFirstScene True when called from boot (skips clearScene / free).
|
||||||
|
void loadScene(psyqo::GPU& gpu, int sceneIndex, bool isFirstScene = false);
|
||||||
|
|
||||||
|
// Check and process pending scene load (called from GameTick)
|
||||||
|
void processPendingSceneLoad();
|
||||||
|
|
||||||
|
private:
|
||||||
|
psxsplash::Lua L;
|
||||||
|
psxsplash::SplashPackLoader m_loader;
|
||||||
|
CollisionSystem m_collisionSystem;
|
||||||
|
BVHManager m_bvh; // Spatial acceleration for frustum culling
|
||||||
|
NavRegionSystem m_navRegions; // Convex region navigation (v7+)
|
||||||
|
uint16_t m_playerNavRegion = NAV_NO_REGION; // Current nav region for player
|
||||||
|
|
||||||
|
// Scene type and render path: 0=exterior (BVH), 1=interior (room/portal)
|
||||||
|
uint16_t m_sceneType = 0;
|
||||||
|
|
||||||
|
// Room/portal data (v11+ interior scenes). Pointers into splashpack data.
|
||||||
|
const RoomData* m_rooms = nullptr;
|
||||||
|
uint16_t m_roomCount = 0;
|
||||||
|
const PortalData* m_portals = nullptr;
|
||||||
|
uint16_t m_portalCount = 0;
|
||||||
|
const TriangleRef* m_roomTriRefs = nullptr;
|
||||||
|
uint16_t m_roomTriRefCount = 0;
|
||||||
|
|
||||||
|
eastl::vector<LuaFile*> m_luaFiles;
|
||||||
|
eastl::vector<GameObject*> m_gameObjects;
|
||||||
|
|
||||||
|
// Object name table (v9+): parallel to m_gameObjects, points into splashpack data
|
||||||
|
eastl::vector<const char*> m_objectNames;
|
||||||
|
|
||||||
|
// Audio clip name table (v10+): parallel to audio clips, points into splashpack data
|
||||||
|
eastl::vector<const char*> m_audioClipNames;
|
||||||
|
|
||||||
|
// Component arrays
|
||||||
|
eastl::vector<Interactable*> m_interactables;
|
||||||
|
|
||||||
|
// Audio system
|
||||||
|
AudioManager m_audio;
|
||||||
|
|
||||||
|
// Cutscene playback
|
||||||
|
Cutscene m_cutscenes[MAX_CUTSCENES];
|
||||||
|
int m_cutsceneCount = 0;
|
||||||
|
CutscenePlayer m_cutscenePlayer;
|
||||||
|
|
||||||
|
Animation m_animations[MAX_ANIMATIONS];
|
||||||
|
int m_animationCount = 0;
|
||||||
|
AnimationPlayer m_animationPlayer;
|
||||||
|
|
||||||
|
UISystem m_uiSystem;
|
||||||
|
#ifdef PSXSPLASH_MEMOVERLAY
|
||||||
|
MemOverlay m_memOverlay;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
psxsplash::Controls m_controls;
|
||||||
|
|
||||||
|
psxsplash::Camera m_currentCamera;
|
||||||
|
|
||||||
|
psyqo::Vec3 m_playerPosition;
|
||||||
|
psyqo::Angle playerRotationX, playerRotationY, playerRotationZ;
|
||||||
|
|
||||||
|
psyqo::FixedPoint<12, uint16_t> m_playerHeight;
|
||||||
|
|
||||||
|
int32_t m_playerRadius;
|
||||||
|
int32_t m_velocityY;
|
||||||
|
int32_t m_gravityPerFrame;
|
||||||
|
int32_t m_jumpVelocityRaw;
|
||||||
|
bool m_isGrounded;
|
||||||
|
|
||||||
|
// Frame timing
|
||||||
|
uint32_t m_lastFrameTime; // gpu.now() timestamp of previous frame
|
||||||
|
int m_deltaFrames; // Elapsed frame count (1 normally, 2+ if dropped)
|
||||||
|
|
||||||
|
bool freecam = false;
|
||||||
|
bool m_controlsEnabled = true; // Lua can disable all player input
|
||||||
|
bool m_cameraFollowsPlayer = true; // False when scene has no nav regions (freecam/cutscene mode)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
uint8_t* m_currentSceneData = nullptr; // Owned pointer to loaded data
|
||||||
|
uint32_t m_liveDataSize = 0; // Bytes of m_currentSceneData still needed at runtime
|
||||||
|
|
||||||
|
// System update methods (called from GameTick)
|
||||||
|
void updateInteractionSystem();
|
||||||
|
void processEnableDisableEvents();
|
||||||
|
void clearScene(); // Deallocate current scene objects
|
||||||
|
void shrinkBuffer(); // Free pixel/audio bulk data after VRAM/SPU uploads
|
||||||
|
};
|
||||||
|
} // namespace psxsplash
|
||||||
@@ -1,61 +1,452 @@
|
|||||||
#include "splashpack.hh"
|
#include "splashpack.hh"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <EASTL/vector.h>
|
||||||
#include <cstring>
|
|
||||||
#include <memory>
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/gte-registers.hh>
|
||||||
#include <psyqo/primitives/common.hh>
|
#include <psyqo/primitives/common.hh>
|
||||||
|
|
||||||
|
#include "bvh.hh"
|
||||||
|
#include "collision.hh"
|
||||||
#include "gameobject.hh"
|
#include "gameobject.hh"
|
||||||
|
#include "cutscene.hh"
|
||||||
|
#include "lua.h"
|
||||||
#include "mesh.hh"
|
#include "mesh.hh"
|
||||||
|
#include "streq.hh"
|
||||||
|
#include "navregion.hh"
|
||||||
#include "renderer.hh"
|
#include "renderer.hh"
|
||||||
|
|
||||||
namespace psxsplash {
|
namespace psxsplash {
|
||||||
|
|
||||||
eastl::vector<psxsplash::GameObject *> LoadSplashpack(uint8_t *data) {
|
struct SPLASHPACKFileHeader {
|
||||||
|
char magic[2];
|
||||||
|
uint16_t version;
|
||||||
|
uint16_t luaFileCount;
|
||||||
|
uint16_t gameObjectCount;
|
||||||
|
uint16_t textureAtlasCount;
|
||||||
|
uint16_t clutCount;
|
||||||
|
uint16_t colliderCount;
|
||||||
|
uint16_t interactableCount;
|
||||||
|
psyqo::GTE::PackedVec3 playerStartPos;
|
||||||
|
psyqo::GTE::PackedVec3 playerStartRot;
|
||||||
|
psyqo::FixedPoint<12, uint16_t> playerHeight;
|
||||||
|
uint16_t sceneLuaFileIndex;
|
||||||
|
uint16_t bvhNodeCount;
|
||||||
|
uint16_t bvhTriangleRefCount;
|
||||||
|
uint16_t sceneType;
|
||||||
|
uint16_t triggerBoxCount;
|
||||||
|
uint16_t worldCollisionMeshCount;
|
||||||
|
uint16_t worldCollisionTriCount;
|
||||||
|
uint16_t navRegionCount;
|
||||||
|
uint16_t navPortalCount;
|
||||||
|
uint16_t moveSpeed;
|
||||||
|
uint16_t sprintSpeed;
|
||||||
|
uint16_t jumpVelocity;
|
||||||
|
uint16_t gravity;
|
||||||
|
uint16_t playerRadius;
|
||||||
|
uint16_t pad1;
|
||||||
|
uint32_t nameTableOffset;
|
||||||
|
uint16_t audioClipCount;
|
||||||
|
uint16_t pad2;
|
||||||
|
uint32_t audioTableOffset;
|
||||||
|
uint8_t fogEnabled;
|
||||||
|
uint8_t fogR, fogG, fogB;
|
||||||
|
uint8_t fogDensity;
|
||||||
|
uint8_t pad3;
|
||||||
|
uint16_t roomCount;
|
||||||
|
uint16_t portalCount;
|
||||||
|
uint16_t roomTriRefCount;
|
||||||
|
uint16_t cutsceneCount;
|
||||||
|
uint16_t pad4;
|
||||||
|
uint32_t cutsceneTableOffset;
|
||||||
|
uint16_t uiCanvasCount;
|
||||||
|
uint8_t uiFontCount;
|
||||||
|
uint8_t uiPad5;
|
||||||
|
uint32_t uiTableOffset;
|
||||||
|
uint32_t pixelDataOffset;
|
||||||
|
uint16_t animationCount;
|
||||||
|
uint16_t animPad;
|
||||||
|
uint32_t animationTableOffset;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SPLASHPACKFileHeader) == 112, "SPLASHPACKFileHeader must be 112 bytes");
|
||||||
|
|
||||||
|
struct SPLASHPACKTextureAtlas {
|
||||||
|
uint32_t polygonsOffset;
|
||||||
|
uint16_t width, height;
|
||||||
|
uint16_t x, y;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SPLASHPACKClut {
|
||||||
|
uint32_t clutOffset;
|
||||||
|
uint16_t clutPackingX;
|
||||||
|
uint16_t clutPackingY;
|
||||||
|
uint16_t length;
|
||||||
|
uint16_t pad;
|
||||||
|
};
|
||||||
|
|
||||||
|
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(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 >= 17, "Splashpack version too old (need v17+): re-export from SplashEdit");
|
||||||
|
|
||||||
eastl::vector<psxsplash::GameObject *> gameObjects;
|
setup.playerStartPosition = header->playerStartPos;
|
||||||
gameObjects.reserve(header->gameObjectCount);
|
setup.playerStartRotation = header->playerStartRot;
|
||||||
|
setup.playerHeight = header->playerHeight;
|
||||||
|
|
||||||
uint8_t *curentPointer = data + sizeof(psxsplash::SPLASHPACKFileHeader);
|
setup.moveSpeed.value = header->moveSpeed;
|
||||||
|
setup.sprintSpeed.value = header->sprintSpeed;
|
||||||
|
setup.jumpVelocity.value = header->jumpVelocity;
|
||||||
|
setup.gravity.value = header->gravity;
|
||||||
|
setup.playerRadius.value = header->playerRadius;
|
||||||
|
|
||||||
|
setup.luaFiles.reserve(header->luaFileCount);
|
||||||
|
setup.objects.reserve(header->gameObjectCount);
|
||||||
|
setup.colliders.reserve(header->colliderCount);
|
||||||
|
setup.interactables.reserve(header->interactableCount);
|
||||||
|
|
||||||
|
uint8_t *cursor = data + sizeof(SPLASHPACKFileHeader);
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < header->luaFileCount; i++) {
|
||||||
|
psxsplash::LuaFile *luaHeader = reinterpret_cast<psxsplash::LuaFile *>(cursor);
|
||||||
|
luaHeader->luaCode = reinterpret_cast<const char *>(data + luaHeader->luaCodeOffset);
|
||||||
|
setup.luaFiles.push_back(luaHeader);
|
||||||
|
cursor += sizeof(psxsplash::LuaFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.sceneLuaFileIndex = (header->sceneLuaFileIndex == 0xFFFF) ? -1 : (int)header->sceneLuaFileIndex;
|
||||||
|
|
||||||
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
|
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
|
||||||
psxsplash::GameObject *go = reinterpret_cast<psxsplash::GameObject *>(curentPointer);
|
psxsplash::GameObject *go = reinterpret_cast<psxsplash::GameObject *>(cursor);
|
||||||
|
|
||||||
int16_t width = 0;
|
|
||||||
switch ((psyqo::Prim::TPageAttr::ColorMode)((std::bit_cast<short>(go->texture) & 0x0180) >> 7)) {
|
|
||||||
case psyqo::Prim::TPageAttr::ColorMode::Tex4Bits:
|
|
||||||
width = 16;
|
|
||||||
break;
|
|
||||||
case psyqo::Prim::TPageAttr::ColorMode::Tex8Bits:
|
|
||||||
width = 256;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
width = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width > 0) {
|
|
||||||
psxsplash::Renderer::getInstance().vramUpload(go->clut, go->clutX * 16, go->clutY, width, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
go->polygons = reinterpret_cast<psxsplash::Tri *>(data + go->polygonsOffset);
|
go->polygons = reinterpret_cast<psxsplash::Tri *>(data + go->polygonsOffset);
|
||||||
|
setup.objects.push_back(go);
|
||||||
|
cursor += sizeof(psxsplash::GameObject);
|
||||||
|
}
|
||||||
|
|
||||||
gameObjects.push_back(go);
|
for (uint16_t i = 0; i < header->colliderCount; i++) {
|
||||||
curentPointer += sizeof(psxsplash::GameObject);
|
psxsplash::SPLASHPACKCollider *collider = reinterpret_cast<psxsplash::SPLASHPACKCollider *>(cursor);
|
||||||
|
setup.colliders.push_back(collider);
|
||||||
|
cursor += sizeof(psxsplash::SPLASHPACKCollider);
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.triggerBoxes.reserve(header->triggerBoxCount);
|
||||||
|
for (uint16_t i = 0; i < header->triggerBoxCount; i++) {
|
||||||
|
psxsplash::SPLASHPACKTriggerBox *tb = reinterpret_cast<psxsplash::SPLASHPACKTriggerBox *>(cursor);
|
||||||
|
setup.triggerBoxes.push_back(tb);
|
||||||
|
cursor += sizeof(psxsplash::SPLASHPACKTriggerBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->bvhNodeCount > 0) {
|
||||||
|
BVHNode* bvhNodes = reinterpret_cast<BVHNode*>(cursor);
|
||||||
|
cursor += header->bvhNodeCount * sizeof(BVHNode);
|
||||||
|
|
||||||
|
TriangleRef* triangleRefs = reinterpret_cast<TriangleRef*>(cursor);
|
||||||
|
cursor += header->bvhTriangleRefCount * sizeof(TriangleRef);
|
||||||
|
|
||||||
|
setup.bvh.initialize(bvhNodes, header->bvhNodeCount,
|
||||||
|
triangleRefs, header->bvhTriangleRefCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < header->interactableCount; i++) {
|
||||||
|
psxsplash::Interactable *interactable = reinterpret_cast<psxsplash::Interactable *>(cursor);
|
||||||
|
setup.interactables.push_back(interactable);
|
||||||
|
cursor += sizeof(psxsplash::Interactable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip over legacy world collision data if present in older binaries
|
||||||
|
if (header->worldCollisionMeshCount > 0) {
|
||||||
|
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
|
||||||
|
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
|
||||||
|
// CollisionDataHeader: 20 bytes
|
||||||
|
const uint16_t meshCount = *reinterpret_cast<const uint16_t*>(cursor);
|
||||||
|
const uint16_t triCount = *reinterpret_cast<const uint16_t*>(cursor + 2);
|
||||||
|
const uint16_t chunkW = *reinterpret_cast<const uint16_t*>(cursor + 4);
|
||||||
|
const uint16_t chunkH = *reinterpret_cast<const uint16_t*>(cursor + 6);
|
||||||
|
cursor += 20; // CollisionDataHeader
|
||||||
|
cursor += meshCount * 32; // CollisionMeshHeader (32 bytes each)
|
||||||
|
cursor += triCount * 52; // CollisionTri (52 bytes each)
|
||||||
|
if (chunkW > 0 && chunkH > 0)
|
||||||
|
cursor += chunkW * chunkH * 4; // CollisionChunk (4 bytes each)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->navRegionCount > 0) {
|
||||||
|
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
|
||||||
|
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
|
||||||
|
cursor = const_cast<uint8_t*>(setup.navRegions.initializeFromData(cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->roomCount > 0) {
|
||||||
|
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
|
||||||
|
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
|
||||||
|
|
||||||
|
setup.rooms = reinterpret_cast<const RoomData*>(cursor);
|
||||||
|
setup.roomCount = header->roomCount;
|
||||||
|
cursor += header->roomCount * sizeof(RoomData);
|
||||||
|
|
||||||
|
setup.portals = reinterpret_cast<const PortalData*>(cursor);
|
||||||
|
setup.portalCount = header->portalCount;
|
||||||
|
cursor += header->portalCount * sizeof(PortalData);
|
||||||
|
|
||||||
|
setup.roomTriRefs = reinterpret_cast<const TriangleRef*>(cursor);
|
||||||
|
setup.roomTriRefCount = header->roomTriRefCount;
|
||||||
|
cursor += header->roomTriRefCount * sizeof(TriangleRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint16_t i = 0; i < header->textureAtlasCount; i++) {
|
for (uint16_t i = 0; i < header->textureAtlasCount; i++) {
|
||||||
psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(curentPointer);
|
psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(cursor);
|
||||||
|
uint8_t *offsetData = data + atlas->polygonsOffset;
|
||||||
uint8_t *offsetData = data + atlas->polygonsOffset; // Ensure correct byte offset
|
// Ensure 4-byte alignment for DMA transfer. If the exporter
|
||||||
uint16_t *castedData = reinterpret_cast<uint16_t *>(offsetData); // Safe cast
|
// produced an unaligned offset, copy to an aligned temporary.
|
||||||
psxsplash::Renderer::getInstance().vramUpload(castedData, atlas->x, atlas->y, atlas->width, atlas->height);
|
uint32_t pixelBytes = (uint32_t)atlas->width * atlas->height * 2;
|
||||||
curentPointer += sizeof(psxsplash::SPLASHPACKTextureAtlas);
|
if (reinterpret_cast<uintptr_t>(offsetData) & 3) {
|
||||||
|
uint8_t* aligned = new uint8_t[(pixelBytes + 3) & ~3];
|
||||||
|
__builtin_memcpy(aligned, offsetData, pixelBytes);
|
||||||
|
psxsplash::Renderer::GetInstance().VramUpload(
|
||||||
|
reinterpret_cast<uint16_t*>(aligned), atlas->x, atlas->y, atlas->width, atlas->height);
|
||||||
|
delete[] aligned;
|
||||||
|
} else {
|
||||||
|
psxsplash::Renderer::GetInstance().VramUpload(
|
||||||
|
reinterpret_cast<uint16_t*>(offsetData), atlas->x, atlas->y, atlas->width, atlas->height);
|
||||||
|
}
|
||||||
|
cursor += sizeof(psxsplash::SPLASHPACKTextureAtlas);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gameObjects;
|
for (uint16_t i = 0; i < header->clutCount; i++) {
|
||||||
|
psxsplash::SPLASHPACKClut *clut = reinterpret_cast<psxsplash::SPLASHPACKClut *>(cursor);
|
||||||
|
uint8_t *clutOffset = data + clut->clutOffset;
|
||||||
|
// Same alignment guard for CLUT data.
|
||||||
|
uint32_t clutBytes = (uint32_t)clut->length * 2;
|
||||||
|
if (reinterpret_cast<uintptr_t>(clutOffset) & 3) {
|
||||||
|
uint8_t* aligned = new uint8_t[(clutBytes + 3) & ~3];
|
||||||
|
__builtin_memcpy(aligned, clutOffset, clutBytes);
|
||||||
|
psxsplash::Renderer::GetInstance().VramUpload(
|
||||||
|
reinterpret_cast<uint16_t*>(aligned), clut->clutPackingX * 16,
|
||||||
|
clut->clutPackingY, clut->length, 1);
|
||||||
|
delete[] aligned;
|
||||||
|
} else {
|
||||||
|
psxsplash::Renderer::GetInstance().VramUpload(
|
||||||
|
reinterpret_cast<uint16_t*>(clutOffset), clut->clutPackingX * 16,
|
||||||
|
clut->clutPackingY, clut->length, 1);
|
||||||
|
}
|
||||||
|
cursor += sizeof(psxsplash::SPLASHPACKClut);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->nameTableOffset != 0) {
|
||||||
|
uint8_t* nameData = data + header->nameTableOffset;
|
||||||
|
setup.objectNames.reserve(header->gameObjectCount);
|
||||||
|
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
|
||||||
|
uint8_t nameLen = *nameData++;
|
||||||
|
const char* nameStr = reinterpret_cast<const char*>(nameData);
|
||||||
|
setup.objectNames.push_back(nameStr);
|
||||||
|
nameData += nameLen + 1; // +1 for null terminator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->audioClipCount > 0 && header->audioTableOffset != 0) {
|
||||||
|
uint8_t* audioTable = data + header->audioTableOffset;
|
||||||
|
setup.audioClips.reserve(header->audioClipCount);
|
||||||
|
setup.audioClipNames.reserve(header->audioClipCount);
|
||||||
|
for (uint16_t i = 0; i < header->audioClipCount; i++) {
|
||||||
|
uint32_t dataOff = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
|
||||||
|
uint32_t size = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
|
||||||
|
uint16_t rate = *reinterpret_cast<uint16_t*>(audioTable); audioTable += 2;
|
||||||
|
uint8_t loop = *audioTable++;
|
||||||
|
uint8_t nameLen = *audioTable++;
|
||||||
|
uint32_t nameOff = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
|
||||||
|
SplashpackSceneSetup::AudioClipSetup clip;
|
||||||
|
clip.adpcmData = data + dataOff;
|
||||||
|
clip.sizeBytes = size;
|
||||||
|
clip.sampleRate = rate;
|
||||||
|
clip.loop = (loop != 0);
|
||||||
|
clip.name = (nameLen > 0 && nameOff != 0) ? reinterpret_cast<const char*>(data + nameOff) : nullptr;
|
||||||
|
setup.audioClips.push_back(clip);
|
||||||
|
setup.audioClipNames.push_back(clip.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.fogEnabled = header->fogEnabled != 0;
|
||||||
|
setup.fogR = header->fogR;
|
||||||
|
setup.fogG = header->fogG;
|
||||||
|
setup.fogB = header->fogB;
|
||||||
|
setup.fogDensity = header->fogDensity;
|
||||||
|
setup.sceneType = header->sceneType;
|
||||||
|
|
||||||
|
if (header->cutsceneCount > 0 && header->cutsceneTableOffset != 0) {
|
||||||
|
setup.cutsceneCount = 0;
|
||||||
|
uint8_t* tablePtr = data + header->cutsceneTableOffset;
|
||||||
|
int csCount = header->cutsceneCount;
|
||||||
|
if (csCount > MAX_CUTSCENES) csCount = MAX_CUTSCENES;
|
||||||
|
|
||||||
|
for (int ci = 0; ci < csCount; ci++) {
|
||||||
|
// SPLASHPACKCutsceneEntry: 12 bytes
|
||||||
|
uint32_t dataOffset = *reinterpret_cast<uint32_t*>(tablePtr); tablePtr += 4;
|
||||||
|
uint8_t nameLen = *tablePtr++;
|
||||||
|
tablePtr += 3; // pad
|
||||||
|
uint32_t nameOffset = *reinterpret_cast<uint32_t*>(tablePtr); tablePtr += 4;
|
||||||
|
|
||||||
|
Cutscene& cs = setup.loadedCutscenes[ci];
|
||||||
|
cs.name = (nameLen > 0 && nameOffset != 0)
|
||||||
|
? reinterpret_cast<const char*>(data + nameOffset)
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
|
// SPLASHPACKCutscene: 12 bytes at dataOffset
|
||||||
|
uint8_t* csPtr = data + dataOffset;
|
||||||
|
cs.totalFrames = *reinterpret_cast<uint16_t*>(csPtr); csPtr += 2;
|
||||||
|
cs.trackCount = *csPtr++;
|
||||||
|
cs.audioEventCount = *csPtr++;
|
||||||
|
uint32_t tracksOff = *reinterpret_cast<uint32_t*>(csPtr); csPtr += 4;
|
||||||
|
uint32_t audioOff = *reinterpret_cast<uint32_t*>(csPtr); csPtr += 4;
|
||||||
|
|
||||||
|
if (cs.trackCount > MAX_TRACKS) cs.trackCount = MAX_TRACKS;
|
||||||
|
if (cs.audioEventCount > MAX_AUDIO_EVENTS) cs.audioEventCount = MAX_AUDIO_EVENTS;
|
||||||
|
|
||||||
|
// Audio events pointer
|
||||||
|
cs.audioEvents = (cs.audioEventCount > 0 && audioOff != 0)
|
||||||
|
? reinterpret_cast<CutsceneAudioEvent*>(data + audioOff)
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
|
// Parse tracks
|
||||||
|
uint8_t* trackPtr = data + tracksOff;
|
||||||
|
for (uint8_t ti = 0; ti < cs.trackCount; ti++) {
|
||||||
|
CutsceneTrack& track = cs.tracks[ti];
|
||||||
|
|
||||||
|
// SPLASHPACKCutsceneTrack: 12 bytes
|
||||||
|
track.trackType = static_cast<TrackType>(*trackPtr++);
|
||||||
|
track.keyframeCount = *trackPtr++;
|
||||||
|
uint8_t objNameLen = *trackPtr++;
|
||||||
|
trackPtr++; // pad
|
||||||
|
uint32_t objNameOff = *reinterpret_cast<uint32_t*>(trackPtr); trackPtr += 4;
|
||||||
|
uint32_t kfOff = *reinterpret_cast<uint32_t*>(trackPtr); trackPtr += 4;
|
||||||
|
|
||||||
|
// Resolve keyframes pointer
|
||||||
|
track.keyframes = (track.keyframeCount > 0 && kfOff != 0)
|
||||||
|
? reinterpret_cast<CutsceneKeyframe*>(data + kfOff)
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
|
// 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<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++) {
|
||||||
|
if (setup.objectNames[oi] &&
|
||||||
|
streq(setup.objectNames[oi], objName)) {
|
||||||
|
track.target = setup.objects[oi];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If not found, target stays nullptr — track will be skipped at runtime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero out unused track slots
|
||||||
|
for (uint8_t ti = cs.trackCount; ti < MAX_TRACKS; ti++) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.cutsceneCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header->version >= 13) {
|
||||||
|
setup.uiCanvasCount = header->uiCanvasCount;
|
||||||
|
setup.uiFontCount = header->uiFontCount;
|
||||||
|
setup.uiTableOffset = header->uiTableOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation loading (v17+)
|
||||||
|
if (header->animationCount > 0 && header->animationTableOffset != 0) {
|
||||||
|
setup.animationCount = 0;
|
||||||
|
uint8_t* tablePtr = data + header->animationTableOffset;
|
||||||
|
int anCount = header->animationCount;
|
||||||
|
if (anCount > MAX_ANIMATIONS) anCount = MAX_ANIMATIONS;
|
||||||
|
|
||||||
|
for (int ai = 0; ai < anCount; ai++) {
|
||||||
|
// SPLASHPACKAnimationEntry: 12 bytes (same layout as cutscene entry)
|
||||||
|
uint32_t dataOffset = *reinterpret_cast<uint32_t*>(tablePtr); tablePtr += 4;
|
||||||
|
uint8_t nameLen = *tablePtr++;
|
||||||
|
tablePtr += 3; // pad
|
||||||
|
uint32_t nameOffset = *reinterpret_cast<uint32_t*>(tablePtr); tablePtr += 4;
|
||||||
|
|
||||||
|
Animation& an = setup.loadedAnimations[ai];
|
||||||
|
an.name = (nameLen > 0 && nameOffset != 0)
|
||||||
|
? reinterpret_cast<const char*>(data + nameOffset)
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
|
// SPLASHPACKAnimation: 8 bytes (no audio)
|
||||||
|
uint8_t* anPtr = data + dataOffset;
|
||||||
|
an.totalFrames = *reinterpret_cast<uint16_t*>(anPtr); anPtr += 2;
|
||||||
|
an.trackCount = *anPtr++;
|
||||||
|
an.pad = *anPtr++;
|
||||||
|
uint32_t tracksOff = *reinterpret_cast<uint32_t*>(anPtr); anPtr += 4;
|
||||||
|
|
||||||
|
if (an.trackCount > MAX_ANIM_TRACKS) an.trackCount = MAX_ANIM_TRACKS;
|
||||||
|
|
||||||
|
// Parse tracks (same format as cutscene tracks)
|
||||||
|
uint8_t* trackPtr = data + tracksOff;
|
||||||
|
for (uint8_t ti = 0; ti < an.trackCount; ti++) {
|
||||||
|
CutsceneTrack& track = an.tracks[ti];
|
||||||
|
|
||||||
|
track.trackType = static_cast<TrackType>(*trackPtr++);
|
||||||
|
track.keyframeCount = *trackPtr++;
|
||||||
|
uint8_t objNameLen = *trackPtr++;
|
||||||
|
trackPtr++; // pad
|
||||||
|
uint32_t objNameOff = *reinterpret_cast<uint32_t*>(trackPtr); trackPtr += 4;
|
||||||
|
uint32_t kfOff = *reinterpret_cast<uint32_t*>(trackPtr); trackPtr += 4;
|
||||||
|
|
||||||
|
track.keyframes = (track.keyframeCount > 0 && kfOff != 0)
|
||||||
|
? reinterpret_cast<CutsceneKeyframe*>(data + kfOff)
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
|
track.target = nullptr;
|
||||||
|
track.uiHandle = -1;
|
||||||
|
if (objNameLen > 0 && objNameOff != 0) {
|
||||||
|
const char* objName = reinterpret_cast<const char*>(data + objNameOff);
|
||||||
|
bool isUITrack = static_cast<uint8_t>(track.trackType) >= 5;
|
||||||
|
if (isUITrack) {
|
||||||
|
track.target = reinterpret_cast<GameObject*>(const_cast<char*>(objName));
|
||||||
|
} else {
|
||||||
|
for (size_t oi = 0; oi < setup.objectNames.size(); oi++) {
|
||||||
|
if (setup.objectNames[oi] &&
|
||||||
|
streq(setup.objectNames[oi], objName)) {
|
||||||
|
track.target = setup.objects[oi];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero unused track slots
|
||||||
|
for (uint8_t ti = an.trackCount; ti < MAX_ANIM_TRACKS; ti++) {
|
||||||
|
an.tracks[ti].keyframeCount = 0;
|
||||||
|
an.tracks[ti].keyframes = nullptr;
|
||||||
|
an.tracks[ti].target = nullptr;
|
||||||
|
an.tracks[ti].uiHandle = -1;
|
||||||
|
an.tracks[ti].initialValues[0] = 0;
|
||||||
|
an.tracks[ti].initialValues[1] = 0;
|
||||||
|
an.tracks[ti].initialValues[2] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.animationCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.liveDataSize = header->pixelDataOffset;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace psxsplash
|
} // namespace psxsplash
|
||||||
295
src/splashpack.cpp.bal
Normal file
295
src/splashpack.cpp.bal
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
#include "splashpack.hh"
|
||||||
|
|
||||||
|
#include <EASTL/vector.h>
|
||||||
|
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/gte-registers.hh>
|
||||||
|
#include <psyqo/primitives/common.hh>
|
||||||
|
|
||||||
|
#include "bvh.hh"
|
||||||
|
#include "collision.hh"
|
||||||
|
#include "gameobject.hh"
|
||||||
|
#include "lua.h"
|
||||||
|
#include "mesh.hh"
|
||||||
|
#include "worldcollision.hh"
|
||||||
|
#include "navregion.hh"
|
||||||
|
#include "renderer.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
struct SPLASHPACKFileHeader {
|
||||||
|
char magic[2]; // "SP"
|
||||||
|
uint16_t version; // Format version (8 = movement params)
|
||||||
|
uint16_t luaFileCount;
|
||||||
|
uint16_t gameObjectCount;
|
||||||
|
uint16_t navmeshCount;
|
||||||
|
uint16_t textureAtlasCount;
|
||||||
|
uint16_t clutCount;
|
||||||
|
uint16_t colliderCount;
|
||||||
|
psyqo::GTE::PackedVec3 playerStartPos;
|
||||||
|
psyqo::GTE::PackedVec3 playerStartRot;
|
||||||
|
psyqo::FixedPoint<12, uint16_t> playerHeight;
|
||||||
|
uint16_t sceneLuaFileIndex;
|
||||||
|
// Version 3 additions:
|
||||||
|
uint16_t bvhNodeCount;
|
||||||
|
uint16_t bvhTriangleRefCount;
|
||||||
|
// Version 4 additions (component counts):
|
||||||
|
uint16_t interactableCount;
|
||||||
|
uint16_t healthCount;
|
||||||
|
uint16_t timerCount;
|
||||||
|
uint16_t spawnerCount;
|
||||||
|
// Version 5 additions (navgrid):
|
||||||
|
uint16_t hasNavGrid; // 1 if navgrid present, 0 otherwise
|
||||||
|
uint16_t reserved; // Alignment padding
|
||||||
|
// Version 6 additions (AABB + scene type):
|
||||||
|
uint16_t sceneType; // 0 = exterior, 1 = interior
|
||||||
|
uint16_t reserved2; // Alignment padding
|
||||||
|
// Version 7 additions (world collision + nav regions):
|
||||||
|
uint16_t worldCollisionMeshCount;
|
||||||
|
uint16_t worldCollisionTriCount;
|
||||||
|
uint16_t navRegionCount;
|
||||||
|
uint16_t navPortalCount;
|
||||||
|
// Version 8 additions (movement parameters):
|
||||||
|
uint16_t moveSpeed; // fp12 per-frame speed constant
|
||||||
|
uint16_t sprintSpeed; // fp12 per-frame speed constant
|
||||||
|
uint16_t jumpVelocity; // fp12 per-second initial jump velocity
|
||||||
|
uint16_t gravity; // fp12 per-second² downward acceleration
|
||||||
|
uint16_t playerRadius; // fp12 collision radius
|
||||||
|
uint16_t reserved3; // Alignment padding
|
||||||
|
// Version 9 additions (object names):
|
||||||
|
uint32_t nameTableOffset; // Offset to name string table (0 = no names)
|
||||||
|
// Version 10 additions (audio):
|
||||||
|
uint16_t audioClipCount; // Number of audio clips
|
||||||
|
uint16_t reserved4; // Alignment padding
|
||||||
|
uint32_t audioTableOffset; // Offset to audio clip table (0 = no audio)
|
||||||
|
// Version 11 additions (fog + room/portal):
|
||||||
|
uint8_t fogEnabled; // 0 = off, 1 = on
|
||||||
|
uint8_t fogR, fogG, fogB; // Fog color RGB
|
||||||
|
uint8_t fogDensity; // 1-10 density scale
|
||||||
|
uint8_t reserved5; // Alignment
|
||||||
|
uint16_t roomCount; // 0 = no room system (use BVH path)
|
||||||
|
uint16_t portalCount;
|
||||||
|
uint16_t roomTriRefCount;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SPLASHPACKFileHeader) == 96, "SPLASHPACKFileHeader must be 96 bytes");
|
||||||
|
|
||||||
|
struct SPLASHPACKTextureAtlas {
|
||||||
|
uint32_t polygonsOffset;
|
||||||
|
uint16_t width, height;
|
||||||
|
uint16_t x, y;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SPLASHPACKClut {
|
||||||
|
uint32_t clutOffset;
|
||||||
|
uint16_t clutPackingX;
|
||||||
|
uint16_t clutPackingY;
|
||||||
|
uint16_t length;
|
||||||
|
uint16_t pad;
|
||||||
|
};
|
||||||
|
|
||||||
|
void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup) {
|
||||||
|
psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null");
|
||||||
|
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(header->version >= 8, "Splashpack version mismatch: re-export from SplashEdit");
|
||||||
|
|
||||||
|
setup.playerStartPosition = header->playerStartPos;
|
||||||
|
setup.playerStartRotation = header->playerStartRot;
|
||||||
|
setup.playerHeight = header->playerHeight;
|
||||||
|
|
||||||
|
// Movement parameters (v8+)
|
||||||
|
setup.moveSpeed.value = header->moveSpeed;
|
||||||
|
setup.sprintSpeed.value = header->sprintSpeed;
|
||||||
|
setup.jumpVelocity.value = header->jumpVelocity;
|
||||||
|
setup.gravity.value = header->gravity;
|
||||||
|
setup.playerRadius.value = header->playerRadius;
|
||||||
|
|
||||||
|
setup.luaFiles.reserve(header->luaFileCount);
|
||||||
|
setup.objects.reserve(header->gameObjectCount);
|
||||||
|
setup.colliders.reserve(header->colliderCount);
|
||||||
|
|
||||||
|
// Reserve component arrays (version 4+)
|
||||||
|
if (header->version >= 4) {
|
||||||
|
setup.interactables.reserve(header->interactableCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// V10 header = 84 bytes, V11+ = 96 bytes. sizeof() always returns 96,
|
||||||
|
// so we must compute the correct offset for older versions.
|
||||||
|
uint32_t headerSize = (header->version >= 11) ? 96 : 84;
|
||||||
|
uint8_t *cursor = data + headerSize;
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < header->luaFileCount; i++) {
|
||||||
|
psxsplash::LuaFile *luaHeader = reinterpret_cast<psxsplash::LuaFile *>(cursor);
|
||||||
|
luaHeader->luaCode = reinterpret_cast<const char *>(data + luaHeader->luaCodeOffset);
|
||||||
|
setup.luaFiles.push_back(luaHeader);
|
||||||
|
cursor += sizeof(psxsplash::LuaFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sceneLuaFileIndex is stored as uint16_t in header; 0xFFFF means "no scene script" (-1)
|
||||||
|
setup.sceneLuaFileIndex = (header->sceneLuaFileIndex == 0xFFFF) ? -1 : (int)header->sceneLuaFileIndex;
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
|
||||||
|
psxsplash::GameObject *go = reinterpret_cast<psxsplash::GameObject *>(cursor);
|
||||||
|
go->polygons = reinterpret_cast<psxsplash::Tri *>(data + go->polygonsOffset);
|
||||||
|
setup.objects.push_back(go);
|
||||||
|
cursor += sizeof(psxsplash::GameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read collision data (after GameObjects)
|
||||||
|
for (uint16_t i = 0; i < header->colliderCount; i++) {
|
||||||
|
psxsplash::SPLASHPACKCollider *collider = reinterpret_cast<psxsplash::SPLASHPACKCollider *>(cursor);
|
||||||
|
setup.colliders.push_back(collider);
|
||||||
|
cursor += sizeof(psxsplash::SPLASHPACKCollider);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read BVH data (version 3+)
|
||||||
|
if (header->version >= 3 && header->bvhNodeCount > 0) {
|
||||||
|
BVHNode* bvhNodes = reinterpret_cast<BVHNode*>(cursor);
|
||||||
|
cursor += header->bvhNodeCount * sizeof(BVHNode);
|
||||||
|
|
||||||
|
TriangleRef* triangleRefs = reinterpret_cast<TriangleRef*>(cursor);
|
||||||
|
cursor += header->bvhTriangleRefCount * sizeof(TriangleRef);
|
||||||
|
|
||||||
|
setup.bvh.initialize(bvhNodes, header->bvhNodeCount,
|
||||||
|
triangleRefs, header->bvhTriangleRefCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read component data (version 4+)
|
||||||
|
if (header->version >= 4) {
|
||||||
|
// Interactables
|
||||||
|
for (uint16_t i = 0; i < header->interactableCount; i++) {
|
||||||
|
psxsplash::Interactable *interactable = reinterpret_cast<psxsplash::Interactable *>(cursor);
|
||||||
|
setup.interactables.push_back(interactable);
|
||||||
|
cursor += sizeof(psxsplash::Interactable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip health components (legacy, 24 bytes each)
|
||||||
|
cursor += header->healthCount * 24;
|
||||||
|
|
||||||
|
// Skip timers (legacy, 16 bytes each)
|
||||||
|
cursor += header->timerCount * 16;
|
||||||
|
|
||||||
|
// Skip spawners (legacy, 44 bytes each)
|
||||||
|
cursor += header->spawnerCount * 44;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read NavGrid (version 5+ — LEGACY, skip if present)
|
||||||
|
if (header->version >= 5 && header->hasNavGrid) {
|
||||||
|
// Skip NavGrid data: header (16 bytes) + cells
|
||||||
|
// NavGridHeader: 4 int32 = 16 bytes, then gridW*gridH*9 bytes
|
||||||
|
int32_t* navGridHeader = reinterpret_cast<int32_t*>(cursor);
|
||||||
|
int32_t gridW = navGridHeader[2];
|
||||||
|
int32_t gridH = navGridHeader[3];
|
||||||
|
cursor += 16; // header
|
||||||
|
cursor += gridW * gridH * 9; // cells (9 bytes each)
|
||||||
|
// Align to 4 bytes
|
||||||
|
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
|
||||||
|
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read world collision soup (version 7+)
|
||||||
|
if (header->version >= 7 && header->worldCollisionMeshCount > 0) {
|
||||||
|
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
|
||||||
|
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
|
||||||
|
cursor = const_cast<uint8_t*>(setup.worldCollision.initializeFromData(cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read nav regions (version 7+)
|
||||||
|
if (header->version >= 7 && header->navRegionCount > 0) {
|
||||||
|
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
|
||||||
|
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
|
||||||
|
cursor = const_cast<uint8_t*>(setup.navRegions.initializeFromData(cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read room/portal data (version 11+, interior scenes)
|
||||||
|
// Must be read here (after nav regions, before navmesh skip / atlas metadata)
|
||||||
|
// to match the sequential cursor position where the writer places it.
|
||||||
|
if (header->version >= 11 && header->roomCount > 0) {
|
||||||
|
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
|
||||||
|
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
|
||||||
|
|
||||||
|
setup.rooms = reinterpret_cast<const RoomData*>(cursor);
|
||||||
|
setup.roomCount = header->roomCount;
|
||||||
|
cursor += header->roomCount * sizeof(RoomData);
|
||||||
|
|
||||||
|
setup.portals = reinterpret_cast<const PortalData*>(cursor);
|
||||||
|
setup.portalCount = header->portalCount;
|
||||||
|
cursor += header->portalCount * sizeof(PortalData);
|
||||||
|
|
||||||
|
setup.roomTriRefs = reinterpret_cast<const TriangleRef*>(cursor);
|
||||||
|
setup.roomTriRefCount = header->roomTriRefCount;
|
||||||
|
cursor += header->roomTriRefCount * sizeof(TriangleRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip legacy navmesh metadata (still present in v7 files)
|
||||||
|
cursor += header->navmeshCount * 8; // Navmesh struct: 4+2+2 = 8 bytes
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < header->textureAtlasCount; i++) {
|
||||||
|
psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(cursor);
|
||||||
|
uint8_t *offsetData = data + atlas->polygonsOffset;
|
||||||
|
uint16_t *castedData = reinterpret_cast<uint16_t *>(offsetData);
|
||||||
|
psxsplash::Renderer::GetInstance().VramUpload(castedData, atlas->x, atlas->y, atlas->width, atlas->height);
|
||||||
|
cursor += sizeof(psxsplash::SPLASHPACKTextureAtlas);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < header->clutCount; i++) {
|
||||||
|
psxsplash::SPLASHPACKClut *clut = reinterpret_cast<psxsplash::SPLASHPACKClut *>(cursor);
|
||||||
|
uint8_t *clutOffset = data + clut->clutOffset;
|
||||||
|
psxsplash::Renderer::GetInstance().VramUpload((uint16_t *)clutOffset, clut->clutPackingX * 16,
|
||||||
|
clut->clutPackingY, clut->length, 1);
|
||||||
|
cursor += sizeof(psxsplash::SPLASHPACKClut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read object name table (version 9+)
|
||||||
|
if (header->version >= 9 && header->nameTableOffset != 0) {
|
||||||
|
uint8_t* nameData = data + header->nameTableOffset;
|
||||||
|
setup.objectNames.reserve(header->gameObjectCount);
|
||||||
|
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
|
||||||
|
uint8_t nameLen = *nameData++;
|
||||||
|
const char* nameStr = reinterpret_cast<const char*>(nameData);
|
||||||
|
// Names are stored as length-prefixed, null-terminated strings
|
||||||
|
setup.objectNames.push_back(nameStr);
|
||||||
|
nameData += nameLen + 1; // +1 for null terminator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read audio clip table (version 10+)
|
||||||
|
if (header->version >= 10 && header->audioClipCount > 0 && header->audioTableOffset != 0) {
|
||||||
|
// Audio table: per clip: uint32_t dataOffset, uint32_t sizeBytes, uint16_t sampleRate, uint8_t loop, uint8_t nameLen, uint32_t nameOffset
|
||||||
|
// Total 16 bytes per entry
|
||||||
|
uint8_t* audioTable = data + header->audioTableOffset;
|
||||||
|
setup.audioClips.reserve(header->audioClipCount);
|
||||||
|
setup.audioClipNames.reserve(header->audioClipCount);
|
||||||
|
for (uint16_t i = 0; i < header->audioClipCount; i++) {
|
||||||
|
uint32_t dataOff = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
|
||||||
|
uint32_t size = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
|
||||||
|
uint16_t rate = *reinterpret_cast<uint16_t*>(audioTable); audioTable += 2;
|
||||||
|
uint8_t loop = *audioTable++;
|
||||||
|
uint8_t nameLen = *audioTable++;
|
||||||
|
uint32_t nameOff = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
|
||||||
|
SplashpackSceneSetup::AudioClipSetup clip;
|
||||||
|
clip.adpcmData = data + dataOff;
|
||||||
|
clip.sizeBytes = size;
|
||||||
|
clip.sampleRate = rate;
|
||||||
|
clip.loop = (loop != 0);
|
||||||
|
clip.name = (nameLen > 0 && nameOff != 0) ? reinterpret_cast<const char*>(data + nameOff) : nullptr;
|
||||||
|
setup.audioClips.push_back(clip);
|
||||||
|
setup.audioClipNames.push_back(clip.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read fog configuration (version 11+)
|
||||||
|
if (header->version >= 11) {
|
||||||
|
setup.fogEnabled = header->fogEnabled != 0;
|
||||||
|
setup.fogR = header->fogR;
|
||||||
|
setup.fogG = header->fogG;
|
||||||
|
setup.fogB = header->fogB;
|
||||||
|
setup.fogDensity = header->fogDensity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read scene type (version 6+ stored it but it was never read until now)
|
||||||
|
setup.sceneType = header->sceneType;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
@@ -2,25 +2,113 @@
|
|||||||
|
|
||||||
#include <EASTL/vector.h>
|
#include <EASTL/vector.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <psyqo/fixed-point.hh>
|
||||||
|
|
||||||
|
#include "bvh.hh"
|
||||||
|
#include "collision.hh"
|
||||||
#include "gameobject.hh"
|
#include "gameobject.hh"
|
||||||
|
#include "lua.h"
|
||||||
|
#include "navregion.hh"
|
||||||
|
#include "audiomanager.hh"
|
||||||
|
#include "interactable.hh"
|
||||||
|
#include "cutscene.hh"
|
||||||
|
#include "animation.hh"
|
||||||
|
#include "uisystem.hh"
|
||||||
|
|
||||||
namespace psxsplash {
|
namespace psxsplash {
|
||||||
|
|
||||||
struct SPLASHPACKFileHeader {
|
/**
|
||||||
char magic[2];
|
* Collision data as stored in the binary file (fixed layout for serialization)
|
||||||
uint16_t version;
|
*/
|
||||||
uint16_t gameObjectCount;
|
struct SPLASHPACKCollider {
|
||||||
uint16_t textureAtlasCount;
|
// AABB bounds in fixed-point (24 bytes)
|
||||||
|
int32_t minX, minY, minZ;
|
||||||
|
int32_t maxX, maxY, maxZ;
|
||||||
|
// Collision metadata (8 bytes)
|
||||||
|
uint8_t collisionType;
|
||||||
|
uint8_t layerMask;
|
||||||
|
uint16_t gameObjectIndex;
|
||||||
|
uint32_t padding;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SPLASHPACKCollider) == 32, "SPLASHPACKCollider must be 32 bytes");
|
||||||
|
|
||||||
|
struct SPLASHPACKTriggerBox {
|
||||||
|
int32_t minX, minY, minZ;
|
||||||
|
int32_t maxX, maxY, maxZ;
|
||||||
|
int16_t luaFileIndex;
|
||||||
|
uint16_t padding;
|
||||||
|
uint32_t padding2;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(SPLASHPACKTriggerBox) == 32, "SPLASHPACKTriggerBox must be 32 bytes");
|
||||||
|
|
||||||
|
struct SplashpackSceneSetup {
|
||||||
|
int sceneLuaFileIndex;
|
||||||
|
eastl::vector<LuaFile *> luaFiles;
|
||||||
|
eastl::vector<GameObject *> objects;
|
||||||
|
eastl::vector<SPLASHPACKCollider *> colliders;
|
||||||
|
eastl::vector<SPLASHPACKTriggerBox *> triggerBoxes;
|
||||||
|
|
||||||
|
// New component arrays
|
||||||
|
eastl::vector<Interactable *> interactables;
|
||||||
|
|
||||||
|
eastl::vector<const char *> objectNames;
|
||||||
|
|
||||||
|
// Audio clips (v10+): ADPCM data with metadata
|
||||||
|
struct AudioClipSetup {
|
||||||
|
const uint8_t* adpcmData;
|
||||||
|
uint32_t sizeBytes;
|
||||||
|
uint16_t sampleRate;
|
||||||
|
bool loop;
|
||||||
|
const char* name; // Points into splashpack data (null-terminated)
|
||||||
|
};
|
||||||
|
eastl::vector<AudioClipSetup> audioClips;
|
||||||
|
|
||||||
|
eastl::vector<const char*> audioClipNames;
|
||||||
|
|
||||||
|
BVHManager bvh; // Spatial acceleration structure for culling
|
||||||
|
NavRegionSystem navRegions;
|
||||||
|
psyqo::GTE::PackedVec3 playerStartPosition;
|
||||||
|
psyqo::GTE::PackedVec3 playerStartRotation;
|
||||||
|
psyqo::FixedPoint<12, uint16_t> playerHeight;
|
||||||
|
|
||||||
|
// Scene type: 0=exterior (BVH culling), 1=interior (room/portal culling)
|
||||||
|
uint16_t sceneType = 0;
|
||||||
|
|
||||||
|
// Fog configuration (v11+)
|
||||||
|
bool fogEnabled = false;
|
||||||
|
uint8_t fogR = 0, fogG = 0, fogB = 0;
|
||||||
|
uint8_t fogDensity = 5;
|
||||||
|
|
||||||
|
const RoomData* rooms = nullptr;
|
||||||
|
uint16_t roomCount = 0;
|
||||||
|
const PortalData* portals = nullptr;
|
||||||
|
uint16_t portalCount = 0;
|
||||||
|
const TriangleRef* roomTriRefs = nullptr;
|
||||||
|
uint16_t roomTriRefCount = 0;
|
||||||
|
|
||||||
|
psyqo::FixedPoint<12, uint16_t> moveSpeed; // Per-frame speed constant (fp12)
|
||||||
|
psyqo::FixedPoint<12, uint16_t> sprintSpeed; // Per-frame sprint constant (fp12)
|
||||||
|
psyqo::FixedPoint<12, uint16_t> jumpVelocity; // Per-second initial velocity (fp12)
|
||||||
|
psyqo::FixedPoint<12, uint16_t> gravity; // Per-second² acceleration (fp12)
|
||||||
|
psyqo::FixedPoint<12, uint16_t> playerRadius; // Collision radius (fp12)
|
||||||
|
|
||||||
|
Cutscene loadedCutscenes[MAX_CUTSCENES];
|
||||||
|
int cutsceneCount = 0;
|
||||||
|
|
||||||
|
Animation loadedAnimations[MAX_ANIMATIONS];
|
||||||
|
int animationCount = 0;
|
||||||
|
|
||||||
|
uint16_t uiCanvasCount = 0;
|
||||||
|
uint8_t uiFontCount = 0;
|
||||||
|
uint32_t uiTableOffset = 0;
|
||||||
|
|
||||||
|
|
||||||
|
uint32_t liveDataSize = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SPLASHPACKTextureAtlas {
|
class SplashPackLoader {
|
||||||
uint32_t polygonsOffset;
|
public:
|
||||||
uint16_t width, height;
|
void LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup);
|
||||||
uint16_t x, y;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
eastl::vector<GameObject *> LoadSplashpack(uint8_t *data);
|
} // namespace psxsplash
|
||||||
|
|
||||||
}; // namespace psxsplash
|
|
||||||
|
|||||||
10
src/streq.hh
Normal file
10
src/streq.hh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
inline bool streq(const char* a, const char* b) {
|
||||||
|
while (*a && *a == *b) { a++; b++; }
|
||||||
|
return *a == *b;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <psyqo/primitives/common.hh>
|
|
||||||
|
|
||||||
namespace psxsplash {
|
|
||||||
class Texture final {
|
|
||||||
public:
|
|
||||||
psyqo::PrimPieces::TPageAttr m_tpage;
|
|
||||||
uint8_t m_width, m_height;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace psxsplash
|
|
||||||
197
src/triclip.cpp
Normal file
197
src/triclip.cpp
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
#include "triclip.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Screen-space Sutherland-Hodgman clipping
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Interpolate between two ClipVertex at parameter t (0..256 = 0.0..1.0, fp8).
|
||||||
|
// Uses fp8 to avoid overflow with int16 coordinates (int16 * 256 fits int32).
|
||||||
|
static ClipVertex lerpClip(const ClipVertex& a, const ClipVertex& b, int32_t t) {
|
||||||
|
ClipVertex r;
|
||||||
|
r.x = (int16_t)(a.x + (((int32_t)(b.x - a.x) * t) >> 8));
|
||||||
|
r.y = (int16_t)(a.y + (((int32_t)(b.y - a.y) * t) >> 8));
|
||||||
|
r.z = (int16_t)(a.z + (((int32_t)(b.z - a.z) * t) >> 8));
|
||||||
|
r.u = (uint8_t)(a.u + (((int)(b.u) - (int)(a.u)) * t >> 8));
|
||||||
|
r.v = (uint8_t)(a.v + (((int)(b.v) - (int)(a.v)) * t >> 8));
|
||||||
|
r.r = (uint8_t)(a.r + (((int)(b.r) - (int)(a.r)) * t >> 8));
|
||||||
|
r.g = (uint8_t)(a.g + (((int)(b.g) - (int)(a.g)) * t >> 8));
|
||||||
|
r.b = (uint8_t)(a.b + (((int)(b.b) - (int)(a.b)) * t >> 8));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip a polygon (in[] with inCount vertices) against a single edge.
|
||||||
|
// Edge is defined by axis (0=X, 1=Y), sign (+1 or -1), and threshold.
|
||||||
|
// Output written to out[], returns output vertex count.
|
||||||
|
static int clipEdge(const ClipVertex* in, int inCount,
|
||||||
|
ClipVertex* out, int axis, int sign, int16_t threshold) {
|
||||||
|
if (inCount == 0) return 0;
|
||||||
|
int outCount = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < inCount; i++) {
|
||||||
|
const ClipVertex& cur = in[i];
|
||||||
|
const ClipVertex& next = in[(i + 1) % inCount];
|
||||||
|
|
||||||
|
int16_t curVal = (axis == 0) ? cur.x : cur.y;
|
||||||
|
int16_t nextVal = (axis == 0) ? next.x : next.y;
|
||||||
|
|
||||||
|
bool curInside = (sign > 0) ? (curVal <= threshold) : (curVal >= threshold);
|
||||||
|
bool nextInside = (sign > 0) ? (nextVal <= threshold) : (nextVal >= threshold);
|
||||||
|
|
||||||
|
if (curInside) {
|
||||||
|
if (outCount < 8) out[outCount++] = cur;
|
||||||
|
if (!nextInside) {
|
||||||
|
// Exiting: compute intersection
|
||||||
|
int32_t den = (int32_t)nextVal - (int32_t)curVal;
|
||||||
|
if (den != 0) {
|
||||||
|
int32_t t = ((int32_t)(threshold - curVal) << 8) / den;
|
||||||
|
if (t < 0) t = 0;
|
||||||
|
if (t > 256) t = 256;
|
||||||
|
if (outCount < 8) out[outCount++] = lerpClip(cur, next, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (nextInside) {
|
||||||
|
// Entering: compute intersection
|
||||||
|
int32_t den = (int32_t)nextVal - (int32_t)curVal;
|
||||||
|
if (den != 0) {
|
||||||
|
int32_t t = ((int32_t)(threshold - curVal) << 8) / den;
|
||||||
|
if (t < 0) t = 0;
|
||||||
|
if (t > 256) t = 256;
|
||||||
|
if (outCount < 8) out[outCount++] = lerpClip(cur, next, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
int clipTriangle(const ClipVertex& v0, const ClipVertex& v1, const ClipVertex& v2,
|
||||||
|
ClipResult& result) {
|
||||||
|
// Working buffers for Sutherland-Hodgman (max 8 vertices after 4 clips).
|
||||||
|
ClipVertex bufA[8], bufB[8];
|
||||||
|
bufA[0] = v0; bufA[1] = v1; bufA[2] = v2;
|
||||||
|
int count = 3;
|
||||||
|
|
||||||
|
// Clip against 4 edges: left, right, top, bottom.
|
||||||
|
count = clipEdge(bufA, count, bufB, 0, -1, CLIP_LEFT); // X >= CLIP_LEFT
|
||||||
|
count = clipEdge(bufB, count, bufA, 0, +1, CLIP_RIGHT); // X <= CLIP_RIGHT
|
||||||
|
count = clipEdge(bufA, count, bufB, 1, -1, CLIP_TOP); // Y >= CLIP_TOP
|
||||||
|
count = clipEdge(bufB, count, bufA, 1, +1, CLIP_BOTTOM); // Y <= CLIP_BOTTOM
|
||||||
|
|
||||||
|
if (count < 3) return 0;
|
||||||
|
|
||||||
|
// Triangulate the convex polygon into a fan from vertex 0.
|
||||||
|
int triCount = count - 2;
|
||||||
|
if (triCount > MAX_CLIP_TRIS) triCount = MAX_CLIP_TRIS;
|
||||||
|
|
||||||
|
for (int i = 0; i < triCount; i++) {
|
||||||
|
result.verts[i * 3 + 0] = bufA[0];
|
||||||
|
result.verts[i * 3 + 1] = bufA[i + 1];
|
||||||
|
result.verts[i * 3 + 2] = bufA[i + 2];
|
||||||
|
}
|
||||||
|
return triCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Near-plane (3D view-space) clipping
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
ViewVertex lerpViewVertex(const ViewVertex& a, const ViewVertex& b, int32_t t) {
|
||||||
|
ViewVertex r;
|
||||||
|
r.x = a.x + (int32_t)(((int64_t)(b.x - a.x) * t) >> 12);
|
||||||
|
r.y = a.y + (int32_t)(((int64_t)(b.y - a.y) * t) >> 12);
|
||||||
|
r.z = a.z + (int32_t)(((int64_t)(b.z - a.z) * t) >> 12);
|
||||||
|
r.u = (uint8_t)(a.u + (((int)(b.u) - (int)(a.u)) * t >> 12));
|
||||||
|
r.v = (uint8_t)(a.v + (((int)(b.v) - (int)(a.v)) * t >> 12));
|
||||||
|
r.r = (uint8_t)(a.r + (((int)(b.r) - (int)(a.r)) * t >> 12));
|
||||||
|
r.g = (uint8_t)(a.g + (((int)(b.g) - (int)(a.g)) * t >> 12));
|
||||||
|
r.b = (uint8_t)(a.b + (((int)(b.b) - (int)(a.b)) * t >> 12));
|
||||||
|
r.pad = 0;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int32_t nearPlaneT(int32_t zA, int32_t zB) {
|
||||||
|
constexpr int32_t nearZ = (int32_t)NEAR_PLANE_Z << 12;
|
||||||
|
int32_t num = nearZ - zA;
|
||||||
|
int32_t den = zB - zA;
|
||||||
|
if (den == 0) return 0;
|
||||||
|
int32_t absNum = num < 0 ? -num : num;
|
||||||
|
int shift = 0;
|
||||||
|
while (absNum > 0x7FFFF) {
|
||||||
|
absNum >>= 1;
|
||||||
|
shift++;
|
||||||
|
}
|
||||||
|
if (shift > 0) {
|
||||||
|
num >>= shift;
|
||||||
|
den >>= shift;
|
||||||
|
if (den == 0) return num > 0 ? 4096 : 0;
|
||||||
|
}
|
||||||
|
return (num << 12) / den;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool isBehind(int32_t z) {
|
||||||
|
return z < ((int32_t)NEAR_PLANE_Z << 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nearPlaneClip(const ViewVertex& v0, const ViewVertex& v1, const ViewVertex& v2,
|
||||||
|
NearClipResult& result) {
|
||||||
|
bool b0 = isBehind(v0.z);
|
||||||
|
bool b1 = isBehind(v1.z);
|
||||||
|
bool b2 = isBehind(v2.z);
|
||||||
|
int behindCount = (int)b0 + (int)b1 + (int)b2;
|
||||||
|
|
||||||
|
if (behindCount == 3) {
|
||||||
|
result.triCount = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (behindCount == 0) {
|
||||||
|
result.triCount = 1;
|
||||||
|
result.verts[0] = v0;
|
||||||
|
result.verts[1] = v1;
|
||||||
|
result.verts[2] = v2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (behindCount == 1) {
|
||||||
|
const ViewVertex* A;
|
||||||
|
const ViewVertex* B;
|
||||||
|
const ViewVertex* C;
|
||||||
|
if (b0) { A = &v0; B = &v1; C = &v2; }
|
||||||
|
else if (b1) { A = &v1; B = &v2; C = &v0; }
|
||||||
|
else { A = &v2; B = &v0; C = &v1; }
|
||||||
|
|
||||||
|
int32_t tAB = nearPlaneT(A->z, B->z);
|
||||||
|
int32_t tAC = nearPlaneT(A->z, C->z);
|
||||||
|
ViewVertex AB = lerpViewVertex(*A, *B, tAB);
|
||||||
|
ViewVertex AC = lerpViewVertex(*A, *C, tAC);
|
||||||
|
|
||||||
|
result.triCount = 2;
|
||||||
|
result.verts[0] = AB;
|
||||||
|
result.verts[1] = *B;
|
||||||
|
result.verts[2] = *C;
|
||||||
|
result.verts[3] = AB;
|
||||||
|
result.verts[4] = *C;
|
||||||
|
result.verts[5] = AC;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const ViewVertex* A;
|
||||||
|
const ViewVertex* B;
|
||||||
|
const ViewVertex* C;
|
||||||
|
if (!b0) { A = &v0; B = &v1; C = &v2; }
|
||||||
|
else if (!b1) { A = &v1; B = &v2; C = &v0; }
|
||||||
|
else { A = &v2; B = &v0; C = &v1; }
|
||||||
|
|
||||||
|
int32_t tBA = nearPlaneT(B->z, A->z);
|
||||||
|
int32_t tCA = nearPlaneT(C->z, A->z);
|
||||||
|
ViewVertex BA = lerpViewVertex(*B, *A, tBA);
|
||||||
|
ViewVertex CA = lerpViewVertex(*C, *A, tCA);
|
||||||
|
|
||||||
|
result.triCount = 1;
|
||||||
|
result.verts[0] = *A;
|
||||||
|
result.verts[1] = BA;
|
||||||
|
result.verts[2] = CA;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
116
src/triclip.hh
Normal file
116
src/triclip.hh
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/primitives/triangles.hh>
|
||||||
|
#include <psyqo/primitives/common.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Screen-space clipping types and functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Screen-space clip vertex with interpolatable attributes.
|
||||||
|
struct ClipVertex {
|
||||||
|
int16_t x, y, z;
|
||||||
|
uint8_t u, v;
|
||||||
|
uint8_t r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Maximum output triangles from clipping a single triangle against 4 edges.
|
||||||
|
// Sutherland-Hodgman can produce up to 7 vertices -> 5 triangles in a fan.
|
||||||
|
static constexpr int MAX_CLIP_TRIS = 5;
|
||||||
|
|
||||||
|
// Result of screen-space triangle clipping.
|
||||||
|
struct ClipResult {
|
||||||
|
ClipVertex verts[MAX_CLIP_TRIS * 3];
|
||||||
|
};
|
||||||
|
|
||||||
|
// GPU rasterizer limits: max vertex-to-vertex delta.
|
||||||
|
static constexpr int16_t MAX_DELTA_X = 1023;
|
||||||
|
static constexpr int16_t MAX_DELTA_Y = 511;
|
||||||
|
|
||||||
|
// Screen-space clip region. Must be narrower than rasterizer limits (1023x511)
|
||||||
|
// so that any triangle fully inside has safe vertex deltas.
|
||||||
|
// Centered on screen (160,120), extended to half the rasterizer max in each direction.
|
||||||
|
static constexpr int16_t CLIP_LEFT = 160 - 510; // -350
|
||||||
|
static constexpr int16_t CLIP_RIGHT = 160 + 510; // 670
|
||||||
|
static constexpr int16_t CLIP_TOP = 120 - 254; // -134
|
||||||
|
static constexpr int16_t CLIP_BOTTOM = 120 + 254; // 374
|
||||||
|
|
||||||
|
// Check if all 3 vertices are on the same side of any screen edge -> invisible.
|
||||||
|
inline bool isCompletelyOutside(const psyqo::Vertex& v0,
|
||||||
|
const psyqo::Vertex& v1,
|
||||||
|
const psyqo::Vertex& v2) {
|
||||||
|
int16_t x0 = v0.x, x1 = v1.x, x2 = v2.x;
|
||||||
|
int16_t y0 = v0.y, y1 = v1.y, y2 = v2.y;
|
||||||
|
|
||||||
|
if (x0 < CLIP_LEFT && x1 < CLIP_LEFT && x2 < CLIP_LEFT) return true;
|
||||||
|
if (x0 > CLIP_RIGHT && x1 > CLIP_RIGHT && x2 > CLIP_RIGHT) return true;
|
||||||
|
if (y0 < CLIP_TOP && y1 < CLIP_TOP && y2 < CLIP_TOP) return true;
|
||||||
|
if (y0 > CLIP_BOTTOM && y1 > CLIP_BOTTOM && y2 > CLIP_BOTTOM) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any vertex is outside the clip region or vertex deltas exceed
|
||||||
|
// rasterizer limits. If true, the triangle needs screen-space clipping.
|
||||||
|
inline bool needsClipping(const psyqo::Vertex& v0,
|
||||||
|
const psyqo::Vertex& v1,
|
||||||
|
const psyqo::Vertex& v2) {
|
||||||
|
int16_t x0 = v0.x, x1 = v1.x, x2 = v2.x;
|
||||||
|
int16_t y0 = v0.y, y1 = v1.y, y2 = v2.y;
|
||||||
|
|
||||||
|
// Check if any vertex is outside the clip region.
|
||||||
|
if (x0 < CLIP_LEFT || x0 > CLIP_RIGHT ||
|
||||||
|
x1 < CLIP_LEFT || x1 > CLIP_RIGHT ||
|
||||||
|
x2 < CLIP_LEFT || x2 > CLIP_RIGHT ||
|
||||||
|
y0 < CLIP_TOP || y0 > CLIP_BOTTOM ||
|
||||||
|
y1 < CLIP_TOP || y1 > CLIP_BOTTOM ||
|
||||||
|
y2 < CLIP_TOP || y2 > CLIP_BOTTOM) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check vertex-to-vertex deltas against rasterizer limits.
|
||||||
|
int16_t minX = x0, maxX = x0;
|
||||||
|
int16_t minY = y0, maxY = y0;
|
||||||
|
if (x1 < minX) minX = x1; if (x1 > maxX) maxX = x1;
|
||||||
|
if (x2 < minX) minX = x2; if (x2 > maxX) maxX = x2;
|
||||||
|
if (y1 < minY) minY = y1; if (y1 > maxY) maxY = y1;
|
||||||
|
if (y2 < minY) minY = y2; if (y2 > maxY) maxY = y2;
|
||||||
|
|
||||||
|
if ((int32_t)(maxX - minX) > MAX_DELTA_X) return true;
|
||||||
|
if ((int32_t)(maxY - minY) > MAX_DELTA_Y) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sutherland-Hodgman screen-space triangle clipping.
|
||||||
|
// Clips against CLIP_LEFT/RIGHT/TOP/BOTTOM, then triangulates the result.
|
||||||
|
// Returns number of output triangles (0 to MAX_CLIP_TRIS), vertices in result.
|
||||||
|
int clipTriangle(const ClipVertex& v0, const ClipVertex& v1, const ClipVertex& v2,
|
||||||
|
ClipResult& result);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Near-plane (3D view-space) clipping types and functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#define NEAR_PLANE_Z 48
|
||||||
|
#define MAX_NEARCLIP_TRIS 2
|
||||||
|
|
||||||
|
struct ViewVertex {
|
||||||
|
int32_t x, y, z;
|
||||||
|
uint8_t u, v;
|
||||||
|
uint8_t r, g, b;
|
||||||
|
uint8_t pad;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NearClipResult {
|
||||||
|
int triCount;
|
||||||
|
ViewVertex verts[MAX_NEARCLIP_TRIS * 3];
|
||||||
|
};
|
||||||
|
|
||||||
|
int nearPlaneClip(const ViewVertex& v0, const ViewVertex& v1, const ViewVertex& v2,
|
||||||
|
NearClipResult& result);
|
||||||
|
ViewVertex lerpViewVertex(const ViewVertex& a, const ViewVertex& b, int32_t t);
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
275
src/typestring.h
Normal file
275
src/typestring.h
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/*~
|
||||||
|
* Copyright (C) 2015, 2016 George Makrydakis <george@irrequietus.eu>
|
||||||
|
*
|
||||||
|
* The 'typestring' header is a single header C++ library for creating types
|
||||||
|
* to use as type parameters in template instantiations, repository available
|
||||||
|
* at https://github.com/irrequietus/typestring. Conceptually stemming from
|
||||||
|
* own implementation of the same thing (but in a more complicated manner to
|
||||||
|
* be revised) in 'clause': https://github.com/irrequietus/clause.
|
||||||
|
*
|
||||||
|
* File subject to the terms and conditions of the Mozilla Public License v 2.0.
|
||||||
|
* If a copy of the MPLv2 license text was not distributed with this file, you
|
||||||
|
* can obtain it at: http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef IRQUS_TYPESTRING_HH_
|
||||||
|
#define IRQUS_TYPESTRING_HH_
|
||||||
|
|
||||||
|
namespace irqus {
|
||||||
|
|
||||||
|
/*~
|
||||||
|
* @desc A class 'storing' strings into distinct, reusable compile-time types that
|
||||||
|
* can be used as type parameters in a template parameter list.
|
||||||
|
* @tprm C... : char non-type parameter pack whose ordered sequence results
|
||||||
|
* into a specific string.
|
||||||
|
* @note Could have wrapped up everything in a single class, eventually will,
|
||||||
|
* once some compilers fix their class scope lookups! I have added some
|
||||||
|
* utility functions because asides being a fun little project, it is of
|
||||||
|
* use in certain constructs related to template metaprogramming
|
||||||
|
* nonetheless.
|
||||||
|
*/
|
||||||
|
template<char... C>
|
||||||
|
struct typestring final {
|
||||||
|
private:
|
||||||
|
static constexpr char const vals[sizeof...(C)+1] = { C...,'\0' };
|
||||||
|
static constexpr unsigned int sval = sizeof...(C);
|
||||||
|
public:
|
||||||
|
|
||||||
|
static constexpr char const * data() noexcept
|
||||||
|
{ return &vals[0]; }
|
||||||
|
|
||||||
|
static constexpr unsigned int size() noexcept
|
||||||
|
{ return sval; };
|
||||||
|
|
||||||
|
static constexpr char const * cbegin() noexcept
|
||||||
|
{ return &vals[0]; }
|
||||||
|
|
||||||
|
static constexpr char const * cend() noexcept
|
||||||
|
{ return &vals[sval]; }
|
||||||
|
};
|
||||||
|
|
||||||
|
template<char... C>
|
||||||
|
constexpr char const typestring<C...>::vals[sizeof...(C)+1];
|
||||||
|
|
||||||
|
//*~ part 1: preparing the ground, because function templates are awesome.
|
||||||
|
|
||||||
|
/*~
|
||||||
|
* @note While it is easy to resort to constexpr strings for use in constexpr
|
||||||
|
* metaprogramming, what we want is to convert compile time string in situ
|
||||||
|
* definitions into reusable, distinct types, for use in advanced template
|
||||||
|
* metaprogramming techniques. We want such features because this kind of
|
||||||
|
* metaprogramming constitutes a pure, non-strict, untyped functional
|
||||||
|
* programming language with pattern matching where declarative semantics
|
||||||
|
* can really shine.
|
||||||
|
*
|
||||||
|
* Currently, there is no feature in C++ that offers the opportunity to
|
||||||
|
* use strings as type parameter types themselves, despite there are
|
||||||
|
* several, different library implementations. This implementation is a
|
||||||
|
* fast, short, single-header, stupid-proof solution that works with any
|
||||||
|
* C++11 compliant compiler and up, with the resulting type being easily
|
||||||
|
* reusable throughout the code.
|
||||||
|
*
|
||||||
|
* @usge Just include the header and enable -std=c++11 or -std=c++14 etc, use
|
||||||
|
* like in the following example:
|
||||||
|
*
|
||||||
|
* typestring_is("Hello!")
|
||||||
|
*
|
||||||
|
* is essentially identical to the following template instantiation:
|
||||||
|
*
|
||||||
|
* irqus::typestring<'H', 'e', 'l', 'l', 'o', '!'>
|
||||||
|
*
|
||||||
|
* By passing -DUSE_TYPESTRING=<power of 2> during compilation, you can
|
||||||
|
* set the maximum length of the 'typestring' from 1 to 1024 (2^0 to 2^10).
|
||||||
|
* Although all preprocessor implementations tested are capable of far
|
||||||
|
* more with this method, exceeding this limit may cause internal compiler
|
||||||
|
* errors in most, with at times rather hilarious results.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template<int N, int M>
|
||||||
|
constexpr char tygrab(char const(&c)[M]) noexcept
|
||||||
|
{ return c[N < M ? N : M-1]; }
|
||||||
|
|
||||||
|
//*~ part2: Function template type signatures for type deduction purposes. In
|
||||||
|
// other words, exploiting the functorial nature of parameter packs
|
||||||
|
// while mixing them with an obvious catamorphism through pattern
|
||||||
|
// matching galore (partial ordering in this case in C++ "parlance").
|
||||||
|
|
||||||
|
template<char... X>
|
||||||
|
auto typoke(typestring<X...>) // as is...
|
||||||
|
-> typestring<X...>;
|
||||||
|
|
||||||
|
template<char... X, char... Y>
|
||||||
|
auto typoke(typestring<X...>, typestring<'\0'>, typestring<Y>...)
|
||||||
|
-> typestring<X...>;
|
||||||
|
|
||||||
|
template<char A, char... X, char... Y>
|
||||||
|
auto typoke(typestring<X...>, typestring<A>, typestring<Y>...)
|
||||||
|
-> decltype(typoke(typestring<X...,A>(), typestring<Y>()...));
|
||||||
|
|
||||||
|
template<char... C>
|
||||||
|
auto typeek(typestring<C...>)
|
||||||
|
-> decltype(typoke(typestring<C>()...));
|
||||||
|
|
||||||
|
template<char... A, char... B, typename... X>
|
||||||
|
auto tycat_(typestring<A...>, typestring<B...>, X... x)
|
||||||
|
-> decltype(tycat_(typestring<A..., B...>(), x...));
|
||||||
|
|
||||||
|
template<char... X>
|
||||||
|
auto tycat_(typestring<X...>)
|
||||||
|
-> typestring<X...>;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Some people actually using this header as is asked me to include
|
||||||
|
* a typestring "cat" utility given that it is easy enough to implement.
|
||||||
|
* I have added this functionality through the template alias below. For
|
||||||
|
* the obvious implementation, nothing more to say. All T... must be
|
||||||
|
* of course, "typestrings".
|
||||||
|
*/
|
||||||
|
template<typename... T>
|
||||||
|
using tycat
|
||||||
|
= decltype(tycat_(T()...));
|
||||||
|
|
||||||
|
} /* irqus */
|
||||||
|
|
||||||
|
|
||||||
|
//*~ part3: some necessary code generation using preprocessor metaprogramming!
|
||||||
|
// There is functional nature in preprocessor metaprogramming as well.
|
||||||
|
|
||||||
|
/*~
|
||||||
|
* @note Code generation block. Undoubtedly, the preprocessor implementations
|
||||||
|
* of both clang++ and g++ are relatively competent in producing a
|
||||||
|
* relatively adequate amount of boilerplate for implementing features
|
||||||
|
* that the language itself will probably be having as features in a few
|
||||||
|
* years. At times, like herein, the preprocessor is able to generate
|
||||||
|
* boilerplate *extremely* fast, but over a certain limit the compiler is
|
||||||
|
* incapable of compiling it. For the record, only certain versions of
|
||||||
|
* g++ where capable of going beyond 4K, so I thought of going from base
|
||||||
|
* 16 to base 2 for USE_TYPESTRING power base. For the record, it takes
|
||||||
|
* a few milliseconds to generate boilerplate for several thousands worth
|
||||||
|
* of "string" length through such an 'fmap' like procedure.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 2^0 = 1 */
|
||||||
|
#define TYPESTRING1(n,x) irqus::tygrab<0x##n##0>(x)
|
||||||
|
|
||||||
|
/* 2^1 = 2 */
|
||||||
|
#define TYPESTRING2(n,x) irqus::tygrab<0x##n##0>(x), irqus::tygrab<0x##n##1>(x)
|
||||||
|
|
||||||
|
/* 2^2 = 2 */
|
||||||
|
#define TYPESTRING4(n,x) \
|
||||||
|
irqus::tygrab<0x##n##0>(x), irqus::tygrab<0x##n##1>(x) \
|
||||||
|
, irqus::tygrab<0x##n##2>(x), irqus::tygrab<0x##n##3>(x)
|
||||||
|
|
||||||
|
/* 2^3 = 8 */
|
||||||
|
#define TYPESTRING8(n,x) \
|
||||||
|
irqus::tygrab<0x##n##0>(x), irqus::tygrab<0x##n##1>(x) \
|
||||||
|
, irqus::tygrab<0x##n##2>(x), irqus::tygrab<0x##n##3>(x) \
|
||||||
|
, irqus::tygrab<0x##n##4>(x), irqus::tygrab<0x##n##5>(x) \
|
||||||
|
, irqus::tygrab<0x##n##6>(x), irqus::tygrab<0x##n##7>(x)
|
||||||
|
|
||||||
|
/* 2^4 = 16 */
|
||||||
|
#define TYPESTRING16(n,x) \
|
||||||
|
irqus::tygrab<0x##n##0>(x), irqus::tygrab<0x##n##1>(x) \
|
||||||
|
, irqus::tygrab<0x##n##2>(x), irqus::tygrab<0x##n##3>(x) \
|
||||||
|
, irqus::tygrab<0x##n##4>(x), irqus::tygrab<0x##n##5>(x) \
|
||||||
|
, irqus::tygrab<0x##n##6>(x), irqus::tygrab<0x##n##7>(x) \
|
||||||
|
, irqus::tygrab<0x##n##8>(x), irqus::tygrab<0x##n##9>(x) \
|
||||||
|
, irqus::tygrab<0x##n##A>(x), irqus::tygrab<0x##n##B>(x) \
|
||||||
|
, irqus::tygrab<0x##n##C>(x), irqus::tygrab<0x##n##D>(x) \
|
||||||
|
, irqus::tygrab<0x##n##E>(x), irqus::tygrab<0x##n##F>(x)
|
||||||
|
|
||||||
|
/* 2^5 = 32 */
|
||||||
|
#define TYPESTRING32(n,x) \
|
||||||
|
TYPESTRING16(n##0,x),TYPESTRING16(n##1,x)
|
||||||
|
|
||||||
|
/* 2^6 = 64 */
|
||||||
|
#define TYPESTRING64(n,x) \
|
||||||
|
TYPESTRING16(n##0,x), TYPESTRING16(n##1,x), TYPESTRING16(n##2,x) \
|
||||||
|
, TYPESTRING16(n##3,x)
|
||||||
|
|
||||||
|
/* 2^7 = 128 */
|
||||||
|
#define TYPESTRING128(n,x) \
|
||||||
|
TYPESTRING16(n##0,x), TYPESTRING16(n##1,x), TYPESTRING16(n##2,x) \
|
||||||
|
, TYPESTRING16(n##3,x), TYPESTRING16(n##4,x), TYPESTRING16(n##5,x) \
|
||||||
|
, TYPESTRING16(n##6,x), TYPESTRING16(n##7,x)
|
||||||
|
|
||||||
|
/* 2^8 = 256 */
|
||||||
|
#define TYPESTRING256(n,x) \
|
||||||
|
TYPESTRING16(n##0,x), TYPESTRING16(n##1,x), TYPESTRING16(n##2,x) \
|
||||||
|
, TYPESTRING16(n##3,x), TYPESTRING16(n##4,x), TYPESTRING16(n##5,x) \
|
||||||
|
, TYPESTRING16(n##6,x), TYPESTRING16(n##7,x), TYPESTRING16(n##8,x) \
|
||||||
|
, TYPESTRING16(n##9,x), TYPESTRING16(n##A,x), TYPESTRING16(n##B,x) \
|
||||||
|
, TYPESTRING16(n##C,x), TYPESTRING16(n##D,x), TYPESTRING16(n##E,x) \
|
||||||
|
, TYPESTRING16(n##F,x)
|
||||||
|
|
||||||
|
/* 2^9 = 512 */
|
||||||
|
#define TYPESTRING512(n,x) \
|
||||||
|
TYPESTRING256(n##0,x), TYPESTRING256(n##1,x)
|
||||||
|
|
||||||
|
/* 2^10 = 1024 */
|
||||||
|
#define TYPESTRING1024(n,x) \
|
||||||
|
TYPESTRING256(n##0,x), TYPESTRING256(n##1,x), TYPESTRING256(n##2,x) \
|
||||||
|
, TYPESTRING128(n##3,x), TYPESTRING16(n##38,x), TYPESTRING16(n##39,x) \
|
||||||
|
, TYPESTRING16(n##3A,x), TYPESTRING16(n##3B,x), TYPESTRING16(n##3C,x) \
|
||||||
|
, TYPESTRING16(n##3D,x), TYPESTRING16(n##3E,x), TYPESTRING16(n##3F,x)
|
||||||
|
|
||||||
|
//*~ part4 : Let's give some logic with a -DUSE_TYPESTRING flag!
|
||||||
|
|
||||||
|
#ifdef USE_TYPESTRING
|
||||||
|
#if USE_TYPESTRING == 0
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING1(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 1
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING2(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 2
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING4(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 3
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING8(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 4
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING16(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 5
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING32(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 6
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING64(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 7
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING128(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 8
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING256(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 9
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING512(,x)>()))
|
||||||
|
#elif USE_TYPESTRING == 10
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING1024(,x)>()))
|
||||||
|
#elif USE_TYPESTRING > 10
|
||||||
|
|
||||||
|
#warning !!!: custom typestring length exceeded allowed (1024) !!!
|
||||||
|
#warning !!!: all typestrings to default maximum typestring length of 64 !!!
|
||||||
|
#warning !!!: you can use -DUSE_TYPESTRING=<power of two> to set length !!!
|
||||||
|
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING64(,x)>()))
|
||||||
|
|
||||||
|
#elif USE_TYPESTRING < 0
|
||||||
|
|
||||||
|
#warning !!!: You used USE_TYPESTRING with a negative size specified !!!
|
||||||
|
#warning !!!: all typestrings to default maximum typestring length of 64 !!!
|
||||||
|
#warning !!!: you can use -DUSE_TYPESTRING=<power of two> to set length !!!
|
||||||
|
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING64(,x)>()))
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#define typestring_is(x) \
|
||||||
|
decltype(irqus::typeek(irqus::typestring<TYPESTRING64(,x)>()))
|
||||||
|
#endif
|
||||||
|
#endif /* IRQUS_TYPESTRING_HH_ */
|
||||||
711
src/uisystem.cpp
Normal file
711
src/uisystem.cpp
Normal file
@@ -0,0 +1,711 @@
|
|||||||
|
#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/sprites.hh>
|
||||||
|
#include <psyqo/primitives/triangles.hh>
|
||||||
|
#include "streq.hh"
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Init
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void UISystem::init(psyqo::Font<>& systemFont) {
|
||||||
|
m_systemFont = &systemFont;
|
||||||
|
m_canvasCount = 0;
|
||||||
|
m_elementCount = 0;
|
||||||
|
m_pendingTextCount = 0;
|
||||||
|
m_fontCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Pointer relocation after buffer shrink
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void UISystem::relocate(intptr_t delta) {
|
||||||
|
for (int ci = 0; ci < m_canvasCount; ci++) {
|
||||||
|
if (m_canvases[ci].name && m_canvases[ci].name[0] != '\0')
|
||||||
|
m_canvases[ci].name = reinterpret_cast<const char*>(reinterpret_cast<intptr_t>(m_canvases[ci].name) + delta);
|
||||||
|
}
|
||||||
|
for (int ei = 0; ei < m_elementCount; ei++) {
|
||||||
|
if (m_elements[ei].name && m_elements[ei].name[0] != '\0')
|
||||||
|
m_elements[ei].name = reinterpret_cast<const char*>(reinterpret_cast<intptr_t>(m_elements[ei].name) + delta);
|
||||||
|
}
|
||||||
|
for (int fi = 0; fi < m_fontCount; fi++) {
|
||||||
|
m_fontDescs[fi].pixelData = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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 (112 bytes each, before canvas data) ──
|
||||||
|
// Layout: glyphW(1) glyphH(1) vramX(2) vramY(2) textureH(2)
|
||||||
|
// dataOffset(4) dataSize(4) advanceWidths(96)
|
||||||
|
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;
|
||||||
|
// Read 96 advance width bytes
|
||||||
|
for (int i = 0; i < 96; i++) {
|
||||||
|
fd.advanceWidths[i] = ptr[16 + i];
|
||||||
|
}
|
||||||
|
ptr += 112;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas descriptors follow immediately after font descriptors.
|
||||||
|
// Font pixel data is in the dead zone (at absolute offsets in the descriptors).
|
||||||
|
|
||||||
|
// ── 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 after font descriptors AND pixel data
|
||||||
|
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: {
|
||||||
|
uint8_t fi = el.textData.fontIndex;
|
||||||
|
if (fi > 0 && fi <= m_fontCount) {
|
||||||
|
// Custom font: render proportionally into OT
|
||||||
|
renderProportionalText(fi - 1, x, y,
|
||||||
|
el.colorR, el.colorG, el.colorB,
|
||||||
|
el.textBuf, ot, balloc);
|
||||||
|
} else if (m_pendingTextCount < UI_MAX_ELEMENTS) {
|
||||||
|
// System font: queue for renderText phase (chainprintf)
|
||||||
|
m_pendingTexts[m_pendingTextCount++] = {
|
||||||
|
x, y,
|
||||||
|
el.colorR, el.colorG, el.colorB,
|
||||||
|
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];
|
||||||
|
m_systemFont->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) {
|
||||||
|
// Only used for system font now; custom fonts go through renderProportionalText
|
||||||
|
return m_systemFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Upload white CLUT at font CLUT position (entry 0=transparent, entry 1=white).
|
||||||
|
// Sprite color tinting will produce the desired text color.
|
||||||
|
static const uint16_t whiteCLUT[2] = { 0x0000, 0x7FFF };
|
||||||
|
Renderer::GetInstance().VramUpload(
|
||||||
|
whiteCLUT,
|
||||||
|
(int16_t)fd.vramX, (int16_t)fd.vramY,
|
||||||
|
2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Proportional text rendering (custom fonts)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void UISystem::renderProportionalText(int fontIdx, int16_t x, int16_t y,
|
||||||
|
uint8_t r, uint8_t g, uint8_t b,
|
||||||
|
const char* text,
|
||||||
|
psyqo::OrderingTable<Renderer::ORDERING_TABLE_SIZE>& ot,
|
||||||
|
psyqo::BumpAllocator<Renderer::BUMP_ALLOCATOR_SIZE>& balloc) {
|
||||||
|
UIFontDesc& fd = m_fontDescs[fontIdx];
|
||||||
|
int glyphsPerRow = 256 / fd.glyphW;
|
||||||
|
uint8_t baseV = fd.vramY & 0xFF;
|
||||||
|
|
||||||
|
// TPage for this font's texture page
|
||||||
|
psyqo::PrimPieces::TPageAttr tpageAttr;
|
||||||
|
tpageAttr.setPageX(fd.vramX >> 6);
|
||||||
|
tpageAttr.setPageY(fd.vramY >> 8);
|
||||||
|
tpageAttr.set(psyqo::Prim::TPageAttr::Tex4Bits);
|
||||||
|
tpageAttr.setDithering(false);
|
||||||
|
|
||||||
|
// CLUT reference for this font
|
||||||
|
psyqo::Vertex clutPos = {{.x = (int16_t)fd.vramX, .y = (int16_t)fd.vramY}};
|
||||||
|
psyqo::PrimPieces::ClutIndex clutIdx(clutPos);
|
||||||
|
|
||||||
|
psyqo::Color color = {.r = r, .g = g, .b = b};
|
||||||
|
|
||||||
|
// First: insert all glyph sprites at depth 0
|
||||||
|
int16_t cursorX = x;
|
||||||
|
while (*text) {
|
||||||
|
uint8_t c = (uint8_t)*text++;
|
||||||
|
if (c < 32 || c > 127) c = '?';
|
||||||
|
uint8_t charIdx = c - 32;
|
||||||
|
|
||||||
|
uint8_t advance = fd.advanceWidths[charIdx];
|
||||||
|
|
||||||
|
if (c == ' ') {
|
||||||
|
cursorX += advance;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int charRow = charIdx / glyphsPerRow;
|
||||||
|
int charCol = charIdx % glyphsPerRow;
|
||||||
|
uint8_t u = (uint8_t)(charCol * fd.glyphW);
|
||||||
|
uint8_t v = (uint8_t)(baseV + charRow * fd.glyphH);
|
||||||
|
|
||||||
|
auto& frag = balloc.allocateFragment<psyqo::Prim::Sprite>();
|
||||||
|
frag.primitive.position = {.x = cursorX, .y = y};
|
||||||
|
// Use advance as sprite width for proportional sizing.
|
||||||
|
// The glyph is left-aligned in the cell, so showing advance-width
|
||||||
|
// pixels captures the glyph content with correct spacing.
|
||||||
|
int16_t spriteW = (advance > 0 && advance < fd.glyphW) ? (int16_t)advance : (int16_t)fd.glyphW;
|
||||||
|
frag.primitive.size = {.x = spriteW, .y = (int16_t)fd.glyphH};
|
||||||
|
frag.primitive.setColor(color);
|
||||||
|
psyqo::PrimPieces::TexInfo texInfo;
|
||||||
|
texInfo.u = u;
|
||||||
|
texInfo.v = v;
|
||||||
|
texInfo.clut = clutIdx;
|
||||||
|
frag.primitive.texInfo = texInfo;
|
||||||
|
ot.insert(frag, 0);
|
||||||
|
|
||||||
|
cursorX += advance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then: insert TPage AFTER sprites at the same depth.
|
||||||
|
// OT uses head insertion (LIFO), so TPage ends up rendering BEFORE the sprites.
|
||||||
|
// This ensures each text element's TPage is active for its own sprites only,
|
||||||
|
// even when multiple fonts are on screen simultaneously.
|
||||||
|
auto& tpFrag = balloc.allocateFragment<psyqo::Prim::TPage>();
|
||||||
|
tpFrag.primitive.attr = tpageAttr;
|
||||||
|
ot.insert(tpFrag, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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 && 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 && 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UISystem::getProgressBgColor(int handle, uint8_t& r, uint8_t& g, uint8_t& b) const {
|
||||||
|
if (handle < 0 || handle >= m_elementCount) { r = g = b = 0; return; }
|
||||||
|
const UIElement& el = m_elements[handle];
|
||||||
|
if (el.type != UIElementType::Progress) { r = g = b = 0; return; }
|
||||||
|
r = el.progress.bgR;
|
||||||
|
g = el.progress.bgG;
|
||||||
|
b = el.progress.bgB;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UISystem::getCanvasCount() const {
|
||||||
|
return m_canvasCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIImageData* UISystem::getImageData(int handle) const {
|
||||||
|
if (handle < 0 || handle >= m_elementCount) return nullptr;
|
||||||
|
const UIElement& el = m_elements[handle];
|
||||||
|
if (el.type != UIElementType::Image) return nullptr;
|
||||||
|
return &el.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIFontDesc* UISystem::getFontDesc(int fontIdx) const {
|
||||||
|
if (fontIdx < 0 || fontIdx >= m_fontCount) return nullptr;
|
||||||
|
return &m_fontDescs[fontIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t UISystem::getTextFontIndex(int handle) const {
|
||||||
|
if (handle < 0 || handle >= m_elementCount) return 0;
|
||||||
|
const UIElement& el = m_elements[handle];
|
||||||
|
if (el.type != UIElementType::Text) return 0;
|
||||||
|
return el.textData.fontIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
165
src/uisystem.hh
Normal file
165
src/uisystem.hh
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#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;
|
||||||
|
uint8_t advanceWidths[96]; // per-char advance (ASCII 0x20-0x7F) in pixels
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
/// Must be called AFTER loadFromSplashpack and BEFORE first render.
|
||||||
|
void uploadFonts(psyqo::GPU& gpu);
|
||||||
|
|
||||||
|
void relocate(intptr_t delta);
|
||||||
|
|
||||||
|
// Phase 1: Insert OT primitives for boxes, images, progress bars, and custom font text.
|
||||||
|
// 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 system font 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;
|
||||||
|
void getProgressBgColor(int handle, uint8_t& r, uint8_t& g, uint8_t& b) const;
|
||||||
|
int getCanvasCount() const;
|
||||||
|
|
||||||
|
// Raw accessors for loading-screen direct rendering
|
||||||
|
const UIImageData* getImageData(int handle) const;
|
||||||
|
const UIFontDesc* getFontDesc(int fontIdx) const; // fontIdx = 0-based custom font index
|
||||||
|
uint8_t getTextFontIndex(int handle) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
psyqo::Font<>* m_systemFont = nullptr;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Pending text for system font only (custom fonts render in OT)
|
||||||
|
struct PendingText { int16_t x, y; uint8_t r, g, b; const char* text; };
|
||||||
|
PendingText m_pendingTexts[UI_MAX_ELEMENTS];
|
||||||
|
int m_pendingTextCount = 0;
|
||||||
|
|
||||||
|
/// Resolve which Font to use for system font (fontIndex 0).
|
||||||
|
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);
|
||||||
|
|
||||||
|
void renderProportionalText(int fontIdx, int16_t x, int16_t y,
|
||||||
|
uint8_t r, uint8_t g, uint8_t b,
|
||||||
|
const char* text,
|
||||||
|
psyqo::OrderingTable<Renderer::ORDERING_TABLE_SIZE>& ot,
|
||||||
|
psyqo::BumpAllocator<Renderer::BUMP_ALLOCATOR_SIZE>& balloc);
|
||||||
|
|
||||||
|
static psyqo::PrimPieces::TPageAttr makeTPage(const UIImageData& img);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
13
src/vram_config.h
Normal file
13
src/vram_config.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// LEGACY -> used by loading screen
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// GPU resolution
|
||||||
|
#define VRAM_RES_WIDTH 320
|
||||||
|
#define VRAM_RES_HEIGHT 240
|
||||||
|
#define VRAM_RES_ENUM psyqo::GPU::Resolution::W320
|
||||||
|
#define VRAM_INTERLACE psyqo::GPU::Interlace::PROGRESSIVE
|
||||||
|
|
||||||
|
// Framebuffer layout
|
||||||
|
#define VRAM_DUAL_BUFFER 1
|
||||||
|
#define VRAM_VERTICAL 1
|
||||||
606
src/worldcollision.cpp
Normal file
606
src/worldcollision.cpp
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
#include "worldcollision.hh"
|
||||||
|
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Fixed-point helpers (20.12)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static constexpr int FRAC_BITS = 12;
|
||||||
|
static constexpr int32_t FP_ONE = 1 << FRAC_BITS; // 4096
|
||||||
|
|
||||||
|
// Multiply two 20.12 values → 20.12
|
||||||
|
static inline int32_t fpmul(int32_t a, int32_t b) {
|
||||||
|
return (int32_t)(((int64_t)a * b) >> FRAC_BITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed-point division: returns (a << 12) / b using only 32-bit DIV.
|
||||||
|
// Uses remainder theorem: (a * 4096) / b = (a/b)*4096 + ((a%b)*4096)/b
|
||||||
|
static inline int32_t fpdiv(int32_t a, int32_t b) {
|
||||||
|
if (b == 0) return 0;
|
||||||
|
int32_t q = a / b;
|
||||||
|
int32_t r = a - q * b;
|
||||||
|
// r * FP_ONE is safe when |r| < 524288 (which covers most game values)
|
||||||
|
return q * FP_ONE + (r << FRAC_BITS) / b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dot product of two 3-vectors in 20.12
|
||||||
|
static inline int32_t dot3(int32_t ax, int32_t ay, int32_t az,
|
||||||
|
int32_t bx, int32_t by, int32_t bz) {
|
||||||
|
return (int32_t)((((int64_t)ax * bx) + ((int64_t)ay * by) + ((int64_t)az * bz)) >> FRAC_BITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross product components (each result is 20.12)
|
||||||
|
static inline void cross3(int32_t ax, int32_t ay, int32_t az,
|
||||||
|
int32_t bx, int32_t by, int32_t bz,
|
||||||
|
int32_t& rx, int32_t& ry, int32_t& rz) {
|
||||||
|
rx = (int32_t)(((int64_t)ay * bz - (int64_t)az * by) >> FRAC_BITS);
|
||||||
|
ry = (int32_t)(((int64_t)az * bx - (int64_t)ax * bz) >> FRAC_BITS);
|
||||||
|
rz = (int32_t)(((int64_t)ax * by - (int64_t)ay * bx) >> FRAC_BITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Square root approximation via Newton's method (for 20.12 input)
|
||||||
|
static int32_t fpsqrt(int32_t x) {
|
||||||
|
if (x <= 0) return 0;
|
||||||
|
// Initial guess: shift right by 6 (half of 12 fractional bits, then adjust)
|
||||||
|
int32_t guess = x;
|
||||||
|
// Rough initial guess
|
||||||
|
if (x > FP_ONE * 16) guess = x >> 4;
|
||||||
|
else if (x > FP_ONE) guess = x >> 2;
|
||||||
|
else guess = FP_ONE;
|
||||||
|
|
||||||
|
// Newton iterations: guess = (guess + x/guess) / 2 in fixed-point
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
if (guess == 0) return 0;
|
||||||
|
int32_t div = fpdiv(x, guess);
|
||||||
|
guess = (guess + div) >> 1;
|
||||||
|
}
|
||||||
|
return guess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length squared of vector (result in 20.12, but represents a squared quantity)
|
||||||
|
static inline int32_t lengthSq(int32_t x, int32_t y, int32_t z) {
|
||||||
|
return dot3(x, y, z, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp value to [lo, hi]
|
||||||
|
static inline int32_t fpclamp(int32_t val, int32_t lo, int32_t hi) {
|
||||||
|
if (val < lo) return lo;
|
||||||
|
if (val > hi) return hi;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Initialization
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const uint8_t* WorldCollision::initializeFromData(const uint8_t* data) {
|
||||||
|
// Read header
|
||||||
|
const auto* hdr = reinterpret_cast<const CollisionDataHeader*>(data);
|
||||||
|
m_header = *hdr;
|
||||||
|
data += sizeof(CollisionDataHeader);
|
||||||
|
|
||||||
|
// Mesh headers
|
||||||
|
m_meshes = reinterpret_cast<const CollisionMeshHeader*>(data);
|
||||||
|
data += m_header.meshCount * sizeof(CollisionMeshHeader);
|
||||||
|
|
||||||
|
// Triangles
|
||||||
|
m_triangles = reinterpret_cast<const CollisionTri*>(data);
|
||||||
|
data += m_header.triangleCount * sizeof(CollisionTri);
|
||||||
|
|
||||||
|
// Spatial chunks (exterior only)
|
||||||
|
if (m_header.chunkGridW > 0 && m_header.chunkGridH > 0) {
|
||||||
|
m_chunks = reinterpret_cast<const CollisionChunk*>(data);
|
||||||
|
data += m_header.chunkGridW * m_header.chunkGridH * sizeof(CollisionChunk);
|
||||||
|
} else {
|
||||||
|
m_chunks = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Broad phase: gather candidate meshes
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
int WorldCollision::gatherCandidateMeshes(int32_t posX, int32_t posZ,
|
||||||
|
uint8_t currentRoom,
|
||||||
|
uint16_t* outIndices,
|
||||||
|
int maxIndices) const {
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
if (m_chunks && m_header.chunkGridW > 0) {
|
||||||
|
// Exterior: spatial grid lookup
|
||||||
|
// dividing two 20.12 values gives integer grid coords directly
|
||||||
|
int cx = 0, cz = 0;
|
||||||
|
if (m_header.chunkSize > 0) {
|
||||||
|
cx = (posX - m_header.chunkOriginX) / m_header.chunkSize;
|
||||||
|
cz = (posZ - m_header.chunkOriginZ) / m_header.chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 3x3 neighborhood for robustness
|
||||||
|
for (int dz = -1; dz <= 1 && count < maxIndices; dz++) {
|
||||||
|
for (int dx = -1; dx <= 1 && count < maxIndices; dx++) {
|
||||||
|
int gx = cx + dx;
|
||||||
|
int gz = cz + dz;
|
||||||
|
if (gx < 0 || gx >= m_header.chunkGridW || gz < 0 || gz >= m_header.chunkGridH)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto& chunk = m_chunks[gz * m_header.chunkGridW + gx];
|
||||||
|
for (int i = 0; i < chunk.meshCount && count < maxIndices; i++) {
|
||||||
|
uint16_t mi = chunk.firstMeshIndex + i;
|
||||||
|
if (mi < m_header.meshCount) {
|
||||||
|
// Deduplicate: check if already added
|
||||||
|
bool dup = false;
|
||||||
|
for (int k = 0; k < count; k++) {
|
||||||
|
if (outIndices[k] == mi) { dup = true; break; }
|
||||||
|
}
|
||||||
|
if (!dup) {
|
||||||
|
outIndices[count++] = mi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Interior: filter by room index, or test all if no room system
|
||||||
|
for (uint16_t i = 0; i < m_header.meshCount && count < maxIndices; i++) {
|
||||||
|
if (currentRoom == 0xFF || m_meshes[i].roomIndex == currentRoom ||
|
||||||
|
m_meshes[i].roomIndex == 0xFF) {
|
||||||
|
outIndices[count++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// AABB helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool WorldCollision::aabbOverlap(int32_t aMinX, int32_t aMinY, int32_t aMinZ,
|
||||||
|
int32_t aMaxX, int32_t aMaxY, int32_t aMaxZ,
|
||||||
|
int32_t bMinX, int32_t bMinY, int32_t bMinZ,
|
||||||
|
int32_t bMaxX, int32_t bMaxY, int32_t bMaxZ) {
|
||||||
|
return aMinX <= bMaxX && aMaxX >= bMinX &&
|
||||||
|
aMinY <= bMaxY && aMaxY >= bMinY &&
|
||||||
|
aMinZ <= bMaxZ && aMaxZ >= bMinZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldCollision::sphereToAABB(int32_t cx, int32_t cy, int32_t cz, int32_t r,
|
||||||
|
int32_t& minX, int32_t& minY, int32_t& minZ,
|
||||||
|
int32_t& maxX, int32_t& maxY, int32_t& maxZ) {
|
||||||
|
minX = cx - r; minY = cy - r; minZ = cz - r;
|
||||||
|
maxX = cx + r; maxY = cy + r; maxZ = cz + r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Sphere vs Triangle (closest point approach)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
int32_t WorldCollision::sphereVsTriangle(int32_t cx, int32_t cy, int32_t cz,
|
||||||
|
int32_t radius,
|
||||||
|
const CollisionTri& tri,
|
||||||
|
int32_t& outNx, int32_t& outNy, int32_t& outNz) const {
|
||||||
|
// Compute vector from v0 to sphere center
|
||||||
|
int32_t px = cx - tri.v0x;
|
||||||
|
int32_t py = cy - tri.v0y;
|
||||||
|
int32_t pz = cz - tri.v0z;
|
||||||
|
|
||||||
|
// Project onto triangle plane using precomputed normal
|
||||||
|
int32_t dist = dot3(px, py, pz, tri.nx, tri.ny, tri.nz);
|
||||||
|
|
||||||
|
// Quick reject if too far from plane
|
||||||
|
int32_t absDist = dist >= 0 ? dist : -dist;
|
||||||
|
if (absDist > radius) return 0;
|
||||||
|
|
||||||
|
// Find closest point on triangle to sphere center
|
||||||
|
// Use barycentric coordinates via edge projections
|
||||||
|
|
||||||
|
// Precompute edge dot products for barycentric coords
|
||||||
|
int32_t d00 = dot3(tri.e1x, tri.e1y, tri.e1z, tri.e1x, tri.e1y, tri.e1z);
|
||||||
|
int32_t d01 = dot3(tri.e1x, tri.e1y, tri.e1z, tri.e2x, tri.e2y, tri.e2z);
|
||||||
|
int32_t d11 = dot3(tri.e2x, tri.e2y, tri.e2z, tri.e2x, tri.e2y, tri.e2z);
|
||||||
|
int32_t d20 = dot3(px, py, pz, tri.e1x, tri.e1y, tri.e1z);
|
||||||
|
int32_t d21 = dot3(px, py, pz, tri.e2x, tri.e2y, tri.e2z);
|
||||||
|
|
||||||
|
// Barycentric denom using fpmul (stays in 32-bit)
|
||||||
|
int32_t denom = fpmul(d00, d11) - fpmul(d01, d01);
|
||||||
|
if (denom == 0) return 0; // Degenerate triangle
|
||||||
|
|
||||||
|
// Barycentric numerators (32-bit via fpmul)
|
||||||
|
int32_t uNum = fpmul(d11, d20) - fpmul(d01, d21);
|
||||||
|
int32_t vNum = fpmul(d00, d21) - fpmul(d01, d20);
|
||||||
|
|
||||||
|
// u, v in 20.12 using 32-bit division only
|
||||||
|
int32_t u = fpdiv(uNum, denom);
|
||||||
|
int32_t v = fpdiv(vNum, denom);
|
||||||
|
|
||||||
|
// Clamp to triangle
|
||||||
|
int32_t w = FP_ONE - u - v;
|
||||||
|
|
||||||
|
int32_t closestX, closestY, closestZ;
|
||||||
|
|
||||||
|
if (u >= 0 && v >= 0 && w >= 0) {
|
||||||
|
// Point is inside triangle — closest point is the plane projection
|
||||||
|
closestX = cx - fpmul(dist, tri.nx);
|
||||||
|
closestY = cy - fpmul(dist, tri.ny);
|
||||||
|
closestZ = cz - fpmul(dist, tri.nz);
|
||||||
|
} else {
|
||||||
|
// Point is outside triangle — find closest point on edges/vertices
|
||||||
|
// Check all 3 edges and pick the closest point
|
||||||
|
|
||||||
|
// v1 = v0 + e1, v2 = v0 + e2
|
||||||
|
int32_t v1x = tri.v0x + tri.e1x;
|
||||||
|
int32_t v1y = tri.v0y + tri.e1y;
|
||||||
|
int32_t v1z = tri.v0z + tri.e1z;
|
||||||
|
int32_t v2x = tri.v0x + tri.e2x;
|
||||||
|
int32_t v2y = tri.v0y + tri.e2y;
|
||||||
|
int32_t v2z = tri.v0z + tri.e2z;
|
||||||
|
|
||||||
|
int32_t bestDistSq = 0x7FFFFFFF;
|
||||||
|
closestX = tri.v0x;
|
||||||
|
closestY = tri.v0y;
|
||||||
|
closestZ = tri.v0z;
|
||||||
|
|
||||||
|
// Helper lambda: closest point on segment [A, B] to point P
|
||||||
|
auto closestOnSeg = [&](int32_t ax, int32_t ay, int32_t az,
|
||||||
|
int32_t bx, int32_t by, int32_t bz,
|
||||||
|
int32_t& ox, int32_t& oy, int32_t& oz) {
|
||||||
|
int32_t abx = bx - ax, aby = by - ay, abz = bz - az;
|
||||||
|
int32_t apx = cx - ax, apy = cy - ay, apz = cz - az;
|
||||||
|
int32_t abLen = dot3(abx, aby, abz, abx, aby, abz);
|
||||||
|
if (abLen == 0) { ox = ax; oy = ay; oz = az; return; }
|
||||||
|
int32_t dotAP = dot3(apx, apy, apz, abx, aby, abz);
|
||||||
|
int32_t t = fpclamp(fpdiv(dotAP, abLen), 0, FP_ONE);
|
||||||
|
ox = ax + fpmul(t, abx);
|
||||||
|
oy = ay + fpmul(t, aby);
|
||||||
|
oz = az + fpmul(t, abz);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Edge v0→v1
|
||||||
|
int32_t ex, ey, ez;
|
||||||
|
closestOnSeg(tri.v0x, tri.v0y, tri.v0z, v1x, v1y, v1z, ex, ey, ez);
|
||||||
|
int32_t dx = cx - ex, dy = cy - ey, dz = cz - ez;
|
||||||
|
int32_t dsq = lengthSq(dx, dy, dz);
|
||||||
|
if (dsq < bestDistSq) { bestDistSq = dsq; closestX = ex; closestY = ey; closestZ = ez; }
|
||||||
|
|
||||||
|
// Edge v0→v2
|
||||||
|
closestOnSeg(tri.v0x, tri.v0y, tri.v0z, v2x, v2y, v2z, ex, ey, ez);
|
||||||
|
dx = cx - ex; dy = cy - ey; dz = cz - ez;
|
||||||
|
dsq = lengthSq(dx, dy, dz);
|
||||||
|
if (dsq < bestDistSq) { bestDistSq = dsq; closestX = ex; closestY = ey; closestZ = ez; }
|
||||||
|
|
||||||
|
// Edge v1→v2
|
||||||
|
closestOnSeg(v1x, v1y, v1z, v2x, v2y, v2z, ex, ey, ez);
|
||||||
|
dx = cx - ex; dy = cy - ey; dz = cz - ez;
|
||||||
|
dsq = lengthSq(dx, dy, dz);
|
||||||
|
if (dsq < bestDistSq) { bestDistSq = dsq; closestX = ex; closestY = ey; closestZ = ez; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute vector from closest point to sphere center
|
||||||
|
int32_t nx = cx - closestX;
|
||||||
|
int32_t ny = cy - closestY;
|
||||||
|
int32_t nz = cz - closestZ;
|
||||||
|
|
||||||
|
// Use 64-bit for distance-squared comparison to avoid 20.12 underflow.
|
||||||
|
// With small radii (e.g. radius=12 for 0.3m at GTE100), fpmul(12,12)=0
|
||||||
|
// because 144>>12=0. This caused ALL collisions to silently fail.
|
||||||
|
// Both sides are in the same raw scale (no shift needed for comparison).
|
||||||
|
int64_t rawDistSq = (int64_t)nx * nx + (int64_t)ny * ny + (int64_t)nz * nz;
|
||||||
|
int64_t rawRadSq = (int64_t)radius * radius;
|
||||||
|
|
||||||
|
if (rawDistSq >= rawRadSq || rawDistSq == 0) return 0;
|
||||||
|
|
||||||
|
// For the actual distance value, use fpsqrt on the 20.12 representation.
|
||||||
|
// If the 20.12 value underflows to 0, estimate from 64-bit.
|
||||||
|
int32_t distSq32 = (int32_t)(rawDistSq >> FRAC_BITS);
|
||||||
|
int32_t distance;
|
||||||
|
if (distSq32 > 0) {
|
||||||
|
distance = fpsqrt(distSq32);
|
||||||
|
} else {
|
||||||
|
// Very close collision - distance is sub-unit in 20.12.
|
||||||
|
// Use triangle normal as push direction, penetration = radius.
|
||||||
|
outNx = tri.nx;
|
||||||
|
outNy = tri.ny;
|
||||||
|
outNz = tri.nz;
|
||||||
|
return radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance == 0) {
|
||||||
|
outNx = tri.nx;
|
||||||
|
outNy = tri.ny;
|
||||||
|
outNz = tri.nz;
|
||||||
|
return radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize push direction using 32-bit division only
|
||||||
|
outNx = fpdiv(nx, distance);
|
||||||
|
outNy = fpdiv(ny, distance);
|
||||||
|
outNz = fpdiv(nz, distance);
|
||||||
|
|
||||||
|
return radius - distance; // Penetration depth
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Ray vs Triangle (Möller-Trumbore, fixed-point)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
int32_t WorldCollision::rayVsTriangle(int32_t ox, int32_t oy, int32_t oz,
|
||||||
|
int32_t dx, int32_t dy, int32_t dz,
|
||||||
|
const CollisionTri& tri) const {
|
||||||
|
// h = cross(D, e2)
|
||||||
|
int32_t hx, hy, hz;
|
||||||
|
cross3(dx, dy, dz, tri.e2x, tri.e2y, tri.e2z, hx, hy, hz);
|
||||||
|
|
||||||
|
// a = dot(e1, h)
|
||||||
|
int32_t a = dot3(tri.e1x, tri.e1y, tri.e1z, hx, hy, hz);
|
||||||
|
if (a > -COLLISION_EPSILON && a < COLLISION_EPSILON)
|
||||||
|
return -1; // Ray parallel to triangle
|
||||||
|
|
||||||
|
// f = 1/a — we'll defer the division by working with a as denominator
|
||||||
|
// s = O - v0
|
||||||
|
int32_t sx = ox - tri.v0x;
|
||||||
|
int32_t sy = oy - tri.v0y;
|
||||||
|
int32_t sz = oz - tri.v0z;
|
||||||
|
|
||||||
|
// u = f * dot(s, h) = dot(s, h) / a
|
||||||
|
int32_t sh = dot3(sx, sy, sz, hx, hy, hz);
|
||||||
|
// Check u in [0, 1]: sh/a must be in [0, a] if a > 0, or [a, 0] if a < 0
|
||||||
|
if (a > 0) {
|
||||||
|
if (sh < 0 || sh > a) return -1;
|
||||||
|
} else {
|
||||||
|
if (sh > 0 || sh < a) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// q = cross(s, e1)
|
||||||
|
int32_t qx, qy, qz;
|
||||||
|
cross3(sx, sy, sz, tri.e1x, tri.e1y, tri.e1z, qx, qy, qz);
|
||||||
|
|
||||||
|
// v = f * dot(D, q) = dot(D, q) / a
|
||||||
|
int32_t dq = dot3(dx, dy, dz, qx, qy, qz);
|
||||||
|
if (a > 0) {
|
||||||
|
if (dq < 0 || sh + dq > a) return -1;
|
||||||
|
} else {
|
||||||
|
if (dq > 0 || sh + dq < a) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// t = f * dot(e2, q) = dot(e2, q) / a
|
||||||
|
int32_t eq = dot3(tri.e2x, tri.e2y, tri.e2z, qx, qy, qz);
|
||||||
|
|
||||||
|
// t in 20.12 using 32-bit division only
|
||||||
|
int32_t t = fpdiv(eq, a);
|
||||||
|
|
||||||
|
if (t < COLLISION_EPSILON) return -1; // Behind ray origin
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// High-level: moveAndSlide
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
psyqo::Vec3 WorldCollision::moveAndSlide(const psyqo::Vec3& oldPos,
|
||||||
|
const psyqo::Vec3& newPos,
|
||||||
|
int32_t radius,
|
||||||
|
uint8_t currentRoom) const {
|
||||||
|
if (!isLoaded()) return newPos;
|
||||||
|
|
||||||
|
int32_t posX = newPos.x.raw();
|
||||||
|
int32_t posY = newPos.y.raw();
|
||||||
|
int32_t posZ = newPos.z.raw();
|
||||||
|
|
||||||
|
// Gather candidate meshes
|
||||||
|
uint16_t meshIndices[32];
|
||||||
|
int meshCount = gatherCandidateMeshes(posX, posZ, currentRoom, meshIndices, 32);
|
||||||
|
|
||||||
|
// Sphere AABB for broad phase
|
||||||
|
int32_t sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ;
|
||||||
|
sphereToAABB(posX, posY, posZ, radius + COLLISION_EPSILON,
|
||||||
|
sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ);
|
||||||
|
|
||||||
|
int triTests = 0;
|
||||||
|
int totalCollisions = 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for (int iter = 0; iter < MAX_COLLISION_ITERATIONS; iter++) {
|
||||||
|
bool collided = false;
|
||||||
|
|
||||||
|
for (int mi = 0; mi < meshCount && triTests < MAX_TRI_TESTS_PER_FRAME; mi++) {
|
||||||
|
const auto& mesh = m_meshes[meshIndices[mi]];
|
||||||
|
|
||||||
|
// Broad phase: sphere AABB vs mesh AABB
|
||||||
|
if (!aabbOverlap(sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ,
|
||||||
|
mesh.aabbMinX, mesh.aabbMinY, mesh.aabbMinZ,
|
||||||
|
mesh.aabbMaxX, mesh.aabbMaxY, mesh.aabbMaxZ)) {
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int ti = 0; ti < mesh.triangleCount && triTests < MAX_TRI_TESTS_PER_FRAME; ti++) {
|
||||||
|
const auto& tri = m_triangles[mesh.firstTriangle + ti];
|
||||||
|
triTests++;
|
||||||
|
|
||||||
|
// Skip floor and ceiling triangles — Y is resolved by nav regions.
|
||||||
|
// In PS1 space (Y-down): floor normals have ny < 0, ceiling ny > 0.
|
||||||
|
// If |ny| > walkable slope threshold, it's a floor/ceiling, not a wall.
|
||||||
|
int32_t absNy = tri.ny >= 0 ? tri.ny : -tri.ny;
|
||||||
|
if (absNy > WALKABLE_SLOPE_COS) continue;
|
||||||
|
|
||||||
|
int32_t nx, ny, nz;
|
||||||
|
int32_t pen = sphereVsTriangle(posX, posY, posZ, radius, tri, nx, ny, nz);
|
||||||
|
|
||||||
|
if (pen > 0) {
|
||||||
|
totalCollisions++;
|
||||||
|
// Push out along normal
|
||||||
|
posX += fpmul(pen + COLLISION_EPSILON, nx);
|
||||||
|
posY += fpmul(pen + COLLISION_EPSILON, ny);
|
||||||
|
posZ += fpmul(pen + COLLISION_EPSILON, nz);
|
||||||
|
|
||||||
|
// Update sphere AABB
|
||||||
|
sphereToAABB(posX, posY, posZ, radius + COLLISION_EPSILON,
|
||||||
|
sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ);
|
||||||
|
collided = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collided) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
psyqo::Vec3 result;
|
||||||
|
result.x.value = posX;
|
||||||
|
result.y.value = posY;
|
||||||
|
result.z.value = posZ;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// High-level: groundTrace
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool WorldCollision::groundTrace(const psyqo::Vec3& pos,
|
||||||
|
int32_t maxDist,
|
||||||
|
int32_t& groundY,
|
||||||
|
int32_t& groundNormalY,
|
||||||
|
uint8_t& surfaceFlags,
|
||||||
|
uint8_t currentRoom) const {
|
||||||
|
if (!isLoaded()) return false;
|
||||||
|
|
||||||
|
int32_t ox = pos.x.raw();
|
||||||
|
int32_t oy = pos.y.raw();
|
||||||
|
int32_t oz = pos.z.raw();
|
||||||
|
|
||||||
|
// Ray direction: straight down (positive Y in PS1 space = down)
|
||||||
|
int32_t dx = 0, dy = FP_ONE, dz = 0;
|
||||||
|
|
||||||
|
uint16_t meshIndices[32];
|
||||||
|
int meshCount = gatherCandidateMeshes(ox, oz, currentRoom, meshIndices, 32);
|
||||||
|
|
||||||
|
int32_t bestDist = maxDist;
|
||||||
|
bool hit = false;
|
||||||
|
|
||||||
|
for (int mi = 0; mi < meshCount; mi++) {
|
||||||
|
const auto& mesh = m_meshes[meshIndices[mi]];
|
||||||
|
|
||||||
|
// Quick reject: check if mesh is below us
|
||||||
|
if (mesh.aabbMinY > oy + maxDist) continue;
|
||||||
|
if (mesh.aabbMaxY < oy) continue;
|
||||||
|
if (ox < mesh.aabbMinX || ox > mesh.aabbMaxX) continue;
|
||||||
|
if (oz < mesh.aabbMinZ || oz > mesh.aabbMaxZ) continue;
|
||||||
|
|
||||||
|
for (int ti = 0; ti < mesh.triangleCount; ti++) {
|
||||||
|
const auto& tri = m_triangles[mesh.firstTriangle + ti];
|
||||||
|
|
||||||
|
int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri);
|
||||||
|
if (t >= 0 && t < bestDist) {
|
||||||
|
bestDist = t;
|
||||||
|
groundY = oy + t; // Hit point Y
|
||||||
|
groundNormalY = tri.ny;
|
||||||
|
surfaceFlags = tri.flags;
|
||||||
|
hit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// High-level: ceilingTrace
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool WorldCollision::ceilingTrace(const psyqo::Vec3& pos,
|
||||||
|
int32_t playerHeight,
|
||||||
|
int32_t& ceilingY,
|
||||||
|
uint8_t currentRoom) const {
|
||||||
|
if (!isLoaded()) return false;
|
||||||
|
|
||||||
|
int32_t ox = pos.x.raw();
|
||||||
|
int32_t oy = pos.y.raw();
|
||||||
|
int32_t oz = pos.z.raw();
|
||||||
|
|
||||||
|
// Ray direction: straight up (negative Y in PS1 space)
|
||||||
|
int32_t dx = 0, dy = -FP_ONE, dz = 0;
|
||||||
|
|
||||||
|
uint16_t meshIndices[32];
|
||||||
|
int meshCount = gatherCandidateMeshes(ox, oz, currentRoom, meshIndices, 32);
|
||||||
|
|
||||||
|
int32_t bestDist = playerHeight;
|
||||||
|
bool hit = false;
|
||||||
|
|
||||||
|
for (int mi = 0; mi < meshCount; mi++) {
|
||||||
|
const auto& mesh = m_meshes[meshIndices[mi]];
|
||||||
|
|
||||||
|
if (mesh.aabbMaxY > oy) continue;
|
||||||
|
if (mesh.aabbMinY < oy - playerHeight) continue;
|
||||||
|
if (ox < mesh.aabbMinX || ox > mesh.aabbMaxX) continue;
|
||||||
|
if (oz < mesh.aabbMinZ || oz > mesh.aabbMaxZ) continue;
|
||||||
|
|
||||||
|
for (int ti = 0; ti < mesh.triangleCount; ti++) {
|
||||||
|
const auto& tri = m_triangles[mesh.firstTriangle + ti];
|
||||||
|
|
||||||
|
int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri);
|
||||||
|
if (t >= 0 && t < bestDist) {
|
||||||
|
bestDist = t;
|
||||||
|
ceilingY = oy - t;
|
||||||
|
hit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// High-level: raycast (general purpose)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool WorldCollision::raycast(int32_t ox, int32_t oy, int32_t oz,
|
||||||
|
int32_t dx, int32_t dy, int32_t dz,
|
||||||
|
int32_t maxDist,
|
||||||
|
CollisionHit& hit,
|
||||||
|
uint8_t currentRoom) const {
|
||||||
|
if (!isLoaded()) return false;
|
||||||
|
|
||||||
|
uint16_t meshIndices[32];
|
||||||
|
int meshCount = gatherCandidateMeshes(ox, oz, currentRoom, meshIndices, 32);
|
||||||
|
|
||||||
|
int32_t bestDist = maxDist;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
for (int mi = 0; mi < meshCount; mi++) {
|
||||||
|
const auto& mesh = m_meshes[meshIndices[mi]];
|
||||||
|
|
||||||
|
for (uint16_t ti = 0; ti < mesh.triangleCount; ti++) {
|
||||||
|
uint16_t triIdx = mesh.firstTriangle + ti;
|
||||||
|
const auto& tri = m_triangles[triIdx];
|
||||||
|
|
||||||
|
int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri);
|
||||||
|
if (t >= 0 && t < bestDist) {
|
||||||
|
bestDist = t;
|
||||||
|
hit.pointX = ox + fpmul(t, dx);
|
||||||
|
hit.pointY = oy + fpmul(t, dy);
|
||||||
|
hit.pointZ = oz + fpmul(t, dz);
|
||||||
|
hit.normalX = tri.nx;
|
||||||
|
hit.normalY = tri.ny;
|
||||||
|
hit.normalZ = tri.nz;
|
||||||
|
hit.distance = t;
|
||||||
|
hit.triangleIndex = triIdx;
|
||||||
|
hit.surfaceFlags = tri.flags;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
227
src/worldcollision.hh
Normal file
227
src/worldcollision.hh
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* worldcollision.hh - Player-vs-World Triangle Collision
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* 1. Broad phase: per-mesh AABB reject, then spatial grid (exterior) or
|
||||||
|
* room membership (interior) to narrow candidate meshes.
|
||||||
|
* 2. Narrow phase: per-triangle capsule-vs-triangle sweep.
|
||||||
|
* 3. Response: sliding projection along collision plane.
|
||||||
|
*
|
||||||
|
* All math is fixed-point 20.12. Zero floats. Deterministic at any framerate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <psyqo/fixed-point.hh>
|
||||||
|
#include <psyqo/vector.hh>
|
||||||
|
|
||||||
|
namespace psxsplash {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Surface flags — packed per-triangle, exported from SplashEdit
|
||||||
|
// ============================================================================
|
||||||
|
enum SurfaceFlag : uint8_t {
|
||||||
|
SURFACE_SOLID = 0x01,
|
||||||
|
SURFACE_SLOPE = 0x02,
|
||||||
|
SURFACE_STAIRS = 0x04,
|
||||||
|
SURFACE_NO_WALK = 0x10,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Collision triangle — world-space, pre-transformed, contiguous in memory
|
||||||
|
// 40 bytes each — v0(12) + v1(12) + v2(12) + normal(12) omitted to save
|
||||||
|
// Actually: 40 bytes = v0(12) + edge1(12) + edge2(12) + flags(1) + pad(3)
|
||||||
|
// We store edges for Moller-Trumbore intersection
|
||||||
|
// ============================================================================
|
||||||
|
struct CollisionTri {
|
||||||
|
// Vertex 0 (world-space 20.12 fixed-point)
|
||||||
|
int32_t v0x, v0y, v0z; // 12 bytes
|
||||||
|
// Edge1 = v1 - v0
|
||||||
|
int32_t e1x, e1y, e1z; // 12 bytes
|
||||||
|
// Edge2 = v2 - v0
|
||||||
|
int32_t e2x, e2y, e2z; // 12 bytes
|
||||||
|
// Precomputed face normal (unit-ish, 20.12)
|
||||||
|
int32_t nx, ny, nz; // 12 bytes
|
||||||
|
// Surface properties
|
||||||
|
uint8_t flags; // SurfaceFlag bitmask
|
||||||
|
uint8_t roomIndex; // Room/chunk this tri belongs to (0xFF = none)
|
||||||
|
uint16_t pad; // Alignment
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CollisionTri) == 52, "CollisionTri must be 52 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Collision mesh header — one per collision mesh in the splashpack
|
||||||
|
// The triangles themselves follow contiguously after all headers.
|
||||||
|
// ============================================================================
|
||||||
|
struct CollisionMeshHeader {
|
||||||
|
// World-space AABB for broad-phase rejection (20.12 fixed-point)
|
||||||
|
int32_t aabbMinX, aabbMinY, aabbMinZ; // 12 bytes
|
||||||
|
int32_t aabbMaxX, aabbMaxY, aabbMaxZ; // 12 bytes
|
||||||
|
// Offset into the collision triangle array
|
||||||
|
uint16_t firstTriangle; // Index of first CollisionTri
|
||||||
|
uint16_t triangleCount; // Number of triangles
|
||||||
|
// Room/chunk association
|
||||||
|
uint8_t roomIndex; // Interior room index (0xFF = exterior)
|
||||||
|
uint8_t pad[3];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CollisionMeshHeader) == 32, "CollisionMeshHeader must be 32 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Spatial chunk for exterior scenes — 2D grid over XZ
|
||||||
|
// ============================================================================
|
||||||
|
struct CollisionChunk {
|
||||||
|
uint16_t firstMeshIndex; // Index into CollisionMeshHeader array
|
||||||
|
uint16_t meshCount; // Number of meshes in this chunk
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CollisionChunk) == 4, "CollisionChunk must be 4 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Collision data header — describes the entire collision dataset
|
||||||
|
// ============================================================================
|
||||||
|
struct CollisionDataHeader {
|
||||||
|
uint16_t meshCount; // Number of CollisionMeshHeader entries
|
||||||
|
uint16_t triangleCount; // Total CollisionTri entries
|
||||||
|
uint16_t chunkGridW; // Spatial grid width (0 if interior)
|
||||||
|
uint16_t chunkGridH; // Spatial grid height (0 if interior)
|
||||||
|
int32_t chunkOriginX; // Grid origin X (20.12)
|
||||||
|
int32_t chunkOriginZ; // Grid origin Z (20.12)
|
||||||
|
int32_t chunkSize; // Cell size (20.12)
|
||||||
|
// Total: 20 bytes
|
||||||
|
// Followed by: meshCount * CollisionMeshHeader
|
||||||
|
// triangleCount * CollisionTri
|
||||||
|
// chunkGridW * chunkGridH * CollisionChunk (if exterior)
|
||||||
|
};
|
||||||
|
static_assert(sizeof(CollisionDataHeader) == 20, "CollisionDataHeader must be 20 bytes");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Hit result from collision queries
|
||||||
|
// ============================================================================
|
||||||
|
struct CollisionHit {
|
||||||
|
int32_t pointX, pointY, pointZ; // Hit point (20.12)
|
||||||
|
int32_t normalX, normalY, normalZ; // Hit normal (20.12)
|
||||||
|
int32_t distance; // Distance along ray (20.12)
|
||||||
|
uint16_t triangleIndex; // Which triangle was hit
|
||||||
|
uint8_t surfaceFlags; // SurfaceFlag of hit triangle
|
||||||
|
uint8_t pad;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Maximum slope angle for walkable surfaces
|
||||||
|
// cos(46°) ≈ 0.6947 → in 20.12 fixed-point = 2845
|
||||||
|
// Surfaces with normal.y < this are treated as walls
|
||||||
|
// ============================================================================
|
||||||
|
static constexpr int32_t WALKABLE_SLOPE_COS = 2845; // cos(46°) in 20.12
|
||||||
|
|
||||||
|
// Player collision capsule radius (20.12 fixed-point)
|
||||||
|
// ~0.5 world units at GTEScaling=100 → 0.005 GTE units → 20 in 20.12
|
||||||
|
static constexpr int32_t PLAYER_RADIUS = 20;
|
||||||
|
|
||||||
|
// Small epsilon for collision (20.12)
|
||||||
|
// ≈ 0.01 GTE units
|
||||||
|
static constexpr int32_t COLLISION_EPSILON = 41;
|
||||||
|
|
||||||
|
// Maximum number of collision iterations per frame
|
||||||
|
static constexpr int MAX_COLLISION_ITERATIONS = 8;
|
||||||
|
|
||||||
|
// Maximum triangles to test per frame (budget)
|
||||||
|
static constexpr int MAX_TRI_TESTS_PER_FRAME = 256;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WorldCollision — main collision query interface
|
||||||
|
// Loaded from splashpack data, used by SceneManager every frame
|
||||||
|
// ============================================================================
|
||||||
|
class WorldCollision {
|
||||||
|
public:
|
||||||
|
WorldCollision() = default;
|
||||||
|
|
||||||
|
/// Initialize from splashpack data. Returns pointer past the data.
|
||||||
|
const uint8_t* initializeFromData(const uint8_t* data);
|
||||||
|
|
||||||
|
/// Is collision data loaded?
|
||||||
|
bool isLoaded() const { return m_triangles != nullptr; }
|
||||||
|
|
||||||
|
void relocate(intptr_t delta) {
|
||||||
|
if (m_meshes) m_meshes = reinterpret_cast<const CollisionMeshHeader*>(reinterpret_cast<intptr_t>(m_meshes) + delta);
|
||||||
|
if (m_triangles) m_triangles = reinterpret_cast<const CollisionTri*>(reinterpret_cast<intptr_t>(m_triangles) + delta);
|
||||||
|
if (m_chunks) m_chunks = reinterpret_cast<const CollisionChunk*>(reinterpret_cast<intptr_t>(m_chunks) + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// High-level queries used by the player movement system
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Move a sphere from oldPos to newPos, sliding against world geometry.
|
||||||
|
/// Returns the final valid position after collision response.
|
||||||
|
/// radius is in 20.12 fixed-point.
|
||||||
|
psyqo::Vec3 moveAndSlide(const psyqo::Vec3& oldPos,
|
||||||
|
const psyqo::Vec3& newPos,
|
||||||
|
int32_t radius,
|
||||||
|
uint8_t currentRoom) const;
|
||||||
|
|
||||||
|
/// Cast a ray downward from pos to find the ground.
|
||||||
|
/// Returns true if ground found within maxDist.
|
||||||
|
/// groundY and groundNormal are filled on hit.
|
||||||
|
bool groundTrace(const psyqo::Vec3& pos,
|
||||||
|
int32_t maxDist,
|
||||||
|
int32_t& groundY,
|
||||||
|
int32_t& groundNormalY,
|
||||||
|
uint8_t& surfaceFlags,
|
||||||
|
uint8_t currentRoom) const;
|
||||||
|
|
||||||
|
/// Cast a ray upward to detect ceilings.
|
||||||
|
bool ceilingTrace(const psyqo::Vec3& pos,
|
||||||
|
int32_t playerHeight,
|
||||||
|
int32_t& ceilingY,
|
||||||
|
uint8_t currentRoom) const;
|
||||||
|
|
||||||
|
/// Raycast against collision geometry. Returns true on hit.
|
||||||
|
bool raycast(int32_t ox, int32_t oy, int32_t oz,
|
||||||
|
int32_t dx, int32_t dy, int32_t dz,
|
||||||
|
int32_t maxDist,
|
||||||
|
CollisionHit& hit,
|
||||||
|
uint8_t currentRoom) const;
|
||||||
|
|
||||||
|
/// Get mesh count for debugging
|
||||||
|
uint16_t getMeshCount() const { return m_header.meshCount; }
|
||||||
|
uint16_t getTriangleCount() const { return m_header.triangleCount; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
CollisionDataHeader m_header = {};
|
||||||
|
const CollisionMeshHeader* m_meshes = nullptr;
|
||||||
|
const CollisionTri* m_triangles = nullptr;
|
||||||
|
const CollisionChunk* m_chunks = nullptr; // Only for exterior scenes
|
||||||
|
|
||||||
|
/// Collect candidate mesh indices near a position.
|
||||||
|
/// For exterior: uses spatial grid. For interior: uses roomIndex.
|
||||||
|
int gatherCandidateMeshes(int32_t posX, int32_t posZ,
|
||||||
|
uint8_t currentRoom,
|
||||||
|
uint16_t* outIndices,
|
||||||
|
int maxIndices) const;
|
||||||
|
|
||||||
|
/// Test a sphere against a single triangle. Returns penetration depth (>0 if colliding).
|
||||||
|
/// On collision, fills outNormal with the push-out direction.
|
||||||
|
int32_t sphereVsTriangle(int32_t cx, int32_t cy, int32_t cz,
|
||||||
|
int32_t radius,
|
||||||
|
const CollisionTri& tri,
|
||||||
|
int32_t& outNx, int32_t& outNy, int32_t& outNz) const;
|
||||||
|
|
||||||
|
/// Ray vs triangle (Moller-Trumbore in fixed-point).
|
||||||
|
/// Returns distance along ray (20.12), or -1 if no hit.
|
||||||
|
int32_t rayVsTriangle(int32_t ox, int32_t oy, int32_t oz,
|
||||||
|
int32_t dx, int32_t dy, int32_t dz,
|
||||||
|
const CollisionTri& tri) const;
|
||||||
|
|
||||||
|
/// AABB vs AABB test
|
||||||
|
static bool aabbOverlap(int32_t aMinX, int32_t aMinY, int32_t aMinZ,
|
||||||
|
int32_t aMaxX, int32_t aMaxY, int32_t aMaxZ,
|
||||||
|
int32_t bMinX, int32_t bMinY, int32_t bMinZ,
|
||||||
|
int32_t bMaxX, int32_t bMaxY, int32_t bMaxZ);
|
||||||
|
|
||||||
|
/// Expand a point to an AABB with radius
|
||||||
|
static void sphereToAABB(int32_t cx, int32_t cy, int32_t cz, int32_t r,
|
||||||
|
int32_t& minX, int32_t& minY, int32_t& minZ,
|
||||||
|
int32_t& maxX, int32_t& maxY, int32_t& maxZ);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace psxsplash
|
||||||
2
third_party/nugget
vendored
2
third_party/nugget
vendored
Submodule third_party/nugget updated: e429a934eb...668163091e
34
tools/luac_psx/Makefile
Normal file
34
tools/luac_psx/Makefile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# luac_psx - PS1 Lua bytecode compiler
|
||||||
|
#
|
||||||
|
# Minimal PS1 executable that compiles Lua source to bytecode.
|
||||||
|
# Links only psyqo (allocator, xprintf) and psxlua (full parser).
|
||||||
|
# No psxsplash code, no renderer, no GPU.
|
||||||
|
|
||||||
|
TARGET = luac_psx
|
||||||
|
TYPE = ps-exe
|
||||||
|
|
||||||
|
SRCS = main.c
|
||||||
|
|
||||||
|
# Relative path to nugget root
|
||||||
|
NUGGETDIR = ../../third_party/nugget
|
||||||
|
|
||||||
|
# Lua library (full parser variant - NOT noparser)
|
||||||
|
LUADIR = $(NUGGETDIR)/third_party/psxlua/src
|
||||||
|
LIBRARIES += $(LUADIR)/liblua.a
|
||||||
|
|
||||||
|
# Include paths
|
||||||
|
CPPFLAGS += -I$(LUADIR)
|
||||||
|
CPPFLAGS += -I$(NUGGETDIR)
|
||||||
|
CPPFLAGS += -DLUA_TARGET_PSX
|
||||||
|
|
||||||
|
# psyqo build system (provides libpsyqo.a, linker scripts, toolchain config)
|
||||||
|
include $(NUGGETDIR)/psyqo/psyqo.mk
|
||||||
|
|
||||||
|
# Build liblua.a if not already built
|
||||||
|
$(LUADIR)/liblua.a:
|
||||||
|
$(MAKE) -C $(NUGGETDIR)/third_party/psxlua psx
|
||||||
|
|
||||||
|
clean::
|
||||||
|
$(MAKE) -C $(NUGGETDIR)/third_party/psxlua clean
|
||||||
|
|
||||||
|
.PHONY: $(LUADIR)/liblua.a
|
||||||
324
tools/luac_psx/main.c
Normal file
324
tools/luac_psx/main.c
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
/*
|
||||||
|
* luac_psx - PS1 Lua bytecode compiler
|
||||||
|
*
|
||||||
|
* A minimal PS1 executable that reads Lua source files via PCdrv,
|
||||||
|
* compiles them using psxlua's compiler, and writes bytecode back
|
||||||
|
* via PCdrv. Designed to run inside PCSX-Redux headless mode as
|
||||||
|
* a build tool.
|
||||||
|
*
|
||||||
|
* Links only psyqo (allocator) and psxlua (full parser). No game
|
||||||
|
* code, no renderer, no GPU.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "pcdrv.h"
|
||||||
|
|
||||||
|
#include "lua.h"
|
||||||
|
#include "lauxlib.h"
|
||||||
|
#include "lualib.h"
|
||||||
|
|
||||||
|
/* Internal headers for luaU_dump (supports strip parameter) */
|
||||||
|
#include "lobject.h"
|
||||||
|
#include "lstate.h"
|
||||||
|
#include "lundump.h"
|
||||||
|
|
||||||
|
/* psyqo's xprintf provides sprintf/printf via PS1 syscalls */
|
||||||
|
#include <psyqo/xprintf.h>
|
||||||
|
|
||||||
|
/* psyqo allocator - provides psyqo_realloc and psyqo_free */
|
||||||
|
#include <psyqo/alloc.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Lua runtime support functions.
|
||||||
|
*
|
||||||
|
* psxlua declares these as extern in llibc.h when LUA_TARGET_PSX
|
||||||
|
* is defined. Normally they're provided via linker --defsym redirects
|
||||||
|
* to psyqo functions, but we define them directly here.
|
||||||
|
*/
|
||||||
|
int luaI_sprintf(char *str, const char *format, ...) {
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, format);
|
||||||
|
int ret = vsprintf(str, format, ap);
|
||||||
|
va_end(ap);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void luaI_free(void *ptr) {
|
||||||
|
psyqo_free(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void *luaI_realloc(void *ptr, size_t size) {
|
||||||
|
return psyqo_realloc(ptr, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Maximum source file size: 256KB should be plenty */
|
||||||
|
#define MAX_SOURCE_SIZE (256 * 1024)
|
||||||
|
|
||||||
|
/* Maximum bytecode output size */
|
||||||
|
#define MAX_OUTPUT_SIZE (256 * 1024)
|
||||||
|
|
||||||
|
/* Maximum manifest line length */
|
||||||
|
#define MAX_LINE_LEN 256
|
||||||
|
|
||||||
|
/* Bytecode writer state */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *buf;
|
||||||
|
size_t size;
|
||||||
|
size_t capacity;
|
||||||
|
} WriterState;
|
||||||
|
|
||||||
|
/* lua_dump writer callback - accumulates bytecode into a buffer */
|
||||||
|
static int bytecode_writer(lua_State *L, const void *p, size_t sz, void *ud) {
|
||||||
|
WriterState *ws = (WriterState *)ud;
|
||||||
|
if (ws->size + sz > ws->capacity) {
|
||||||
|
printf("ERROR: bytecode output exceeds buffer capacity\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
/* memcpy via byte loop - no libc on PS1 */
|
||||||
|
const uint8_t *src = (const uint8_t *)p;
|
||||||
|
uint8_t *dst = ws->buf + ws->size;
|
||||||
|
for (size_t i = 0; i < sz; i++) {
|
||||||
|
dst[i] = src[i];
|
||||||
|
}
|
||||||
|
ws->size += sz;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read an entire file via PCdrv into a buffer. Returns bytes read, or -1 on error. */
|
||||||
|
static int read_file(const char *path, char *buf, int max_size) {
|
||||||
|
int fd = PCopen(path, 0, 0); /* O_RDONLY = 0 */
|
||||||
|
if (fd < 0) {
|
||||||
|
printf("ERROR: cannot open '%s'\n", path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
while (total < max_size) {
|
||||||
|
int chunk = max_size - total;
|
||||||
|
if (chunk > 2048) chunk = 2048; /* read in 2KB chunks */
|
||||||
|
int n = PCread(fd, buf + total, chunk);
|
||||||
|
if (n <= 0) break;
|
||||||
|
total += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
PCclose(fd);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write a buffer to a file via PCdrv. Returns 0 on success, -1 on error. */
|
||||||
|
static int write_file(const char *path, const void *buf, int size) {
|
||||||
|
int fd = PCcreat(path, 0);
|
||||||
|
if (fd < 0) {
|
||||||
|
printf("ERROR: cannot create '%s'\n", path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *p = (const uint8_t *)buf;
|
||||||
|
int remaining = size;
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunk = remaining;
|
||||||
|
if (chunk > 2048) chunk = 2048; /* write in 2KB chunks */
|
||||||
|
int n = PCwrite(fd, p, chunk);
|
||||||
|
if (n < 0) {
|
||||||
|
printf("ERROR: write failed for '%s'\n", path);
|
||||||
|
PCclose(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
p += n;
|
||||||
|
remaining -= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
PCclose(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simple strlen - no libc on PS1 */
|
||||||
|
static int str_len(const char *s) {
|
||||||
|
int n = 0;
|
||||||
|
while (s[n]) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error log buffer - accumulates error messages for the sentinel file */
|
||||||
|
static char error_log[4096];
|
||||||
|
static int error_log_len = 0;
|
||||||
|
|
||||||
|
static void error_log_append(const char *msg) {
|
||||||
|
int len = str_len(msg);
|
||||||
|
if (error_log_len + len + 1 < (int)sizeof(error_log)) {
|
||||||
|
const char *src = msg;
|
||||||
|
char *dst = error_log + error_log_len;
|
||||||
|
while (*src) *dst++ = *src++;
|
||||||
|
*dst++ = '\n';
|
||||||
|
*dst = '\0';
|
||||||
|
error_log_len += len + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write the sentinel file to signal completion */
|
||||||
|
static void write_sentinel(const char *status) {
|
||||||
|
/* For errors, write the error log as sentinel content */
|
||||||
|
if (str_len(status) == 5 && status[0] == 'E') {
|
||||||
|
/* "ERROR" - write error details */
|
||||||
|
if (error_log_len > 0)
|
||||||
|
write_file("__done__", error_log, error_log_len);
|
||||||
|
else
|
||||||
|
write_file("__done__", status, str_len(status));
|
||||||
|
} else {
|
||||||
|
write_file("__done__", status, str_len(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse the next line from the manifest buffer. Returns line length, or -1 at end. */
|
||||||
|
static int next_line(const char *buf, int buf_len, int *pos, char *line, int max_line) {
|
||||||
|
if (*pos >= buf_len) return -1;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (*pos < buf_len && i < max_line - 1) {
|
||||||
|
char c = buf[*pos];
|
||||||
|
(*pos)++;
|
||||||
|
if (c == '\n') break;
|
||||||
|
if (c == '\r') continue; /* skip CR */
|
||||||
|
line[i++] = c;
|
||||||
|
}
|
||||||
|
line[i] = '\0';
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compile a single Lua source file to bytecode */
|
||||||
|
static int compile_file(const char *input_path, const char *output_path,
|
||||||
|
char *source_buf, uint8_t *output_buf) {
|
||||||
|
printf(" %s -> %s\n", input_path, output_path);
|
||||||
|
|
||||||
|
/* Read source */
|
||||||
|
int source_len = read_file(input_path, source_buf, MAX_SOURCE_SIZE);
|
||||||
|
if (source_len < 0) {
|
||||||
|
error_log_append("ERROR: cannot open source file");
|
||||||
|
error_log_append(input_path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a fresh Lua state for each file to avoid accumulating memory */
|
||||||
|
lua_State *L = luaL_newstate();
|
||||||
|
if (!L) {
|
||||||
|
printf("ERROR: cannot create Lua state (out of memory)\n");
|
||||||
|
error_log_append("ERROR: cannot create Lua state (out of memory)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compile source to bytecode */
|
||||||
|
int status = luaL_loadbuffer(L, source_buf, source_len, input_path);
|
||||||
|
if (status != LUA_OK) {
|
||||||
|
const char *err = lua_tostring(L, -1);
|
||||||
|
if (err) {
|
||||||
|
printf("ERROR: %s\n", err);
|
||||||
|
error_log_append(err);
|
||||||
|
} else {
|
||||||
|
printf("ERROR: compilation failed for '%s'\n", input_path);
|
||||||
|
error_log_append("ERROR: compilation failed");
|
||||||
|
error_log_append(input_path);
|
||||||
|
}
|
||||||
|
lua_close(L);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dump bytecode (strip debug info to save space) */
|
||||||
|
WriterState ws;
|
||||||
|
ws.buf = output_buf;
|
||||||
|
ws.size = 0;
|
||||||
|
ws.capacity = MAX_OUTPUT_SIZE;
|
||||||
|
|
||||||
|
/* Use luaU_dump directly with strip=1 to remove debug info (line numbers,
|
||||||
|
* local variable names, source filename). Saves significant space. */
|
||||||
|
status = luaU_dump(L, getproto(L->top - 1), bytecode_writer, &ws, 1);
|
||||||
|
lua_close(L);
|
||||||
|
|
||||||
|
if (status != 0) {
|
||||||
|
printf("ERROR: bytecode dump failed for '%s'\n", input_path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write bytecode to output file */
|
||||||
|
if (write_file(output_path, ws.buf, ws.size) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(" OK (%d bytes source -> %d bytes bytecode)\n", source_len, (int)ws.size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
/* Initialize PCdrv */
|
||||||
|
PCinit();
|
||||||
|
|
||||||
|
printf("luac_psx: PS1 Lua bytecode compiler\n");
|
||||||
|
|
||||||
|
/* Allocate work buffers */
|
||||||
|
char *source_buf = (char *)psyqo_realloc(NULL, MAX_SOURCE_SIZE);
|
||||||
|
uint8_t *output_buf = (uint8_t *)psyqo_realloc(NULL, MAX_OUTPUT_SIZE);
|
||||||
|
char *manifest_buf = (char *)psyqo_realloc(NULL, MAX_SOURCE_SIZE);
|
||||||
|
|
||||||
|
if (!source_buf || !output_buf || !manifest_buf) {
|
||||||
|
printf("ERROR: cannot allocate work buffers\n");
|
||||||
|
write_sentinel("ERROR");
|
||||||
|
while (1) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read manifest file */
|
||||||
|
int manifest_len = read_file("manifest.txt", manifest_buf, MAX_SOURCE_SIZE);
|
||||||
|
if (manifest_len < 0) {
|
||||||
|
printf("ERROR: cannot read manifest.txt\n");
|
||||||
|
write_sentinel("ERROR");
|
||||||
|
while (1) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process manifest: pairs of lines (input, output) */
|
||||||
|
int pos = 0;
|
||||||
|
int file_count = 0;
|
||||||
|
int error_count = 0;
|
||||||
|
char input_path[MAX_LINE_LEN];
|
||||||
|
char output_path[MAX_LINE_LEN];
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
/* Read input path */
|
||||||
|
int len = next_line(manifest_buf, manifest_len, &pos, input_path, MAX_LINE_LEN);
|
||||||
|
if (len < 0) break;
|
||||||
|
if (len == 0) continue; /* skip blank lines */
|
||||||
|
|
||||||
|
/* Read output path */
|
||||||
|
len = next_line(manifest_buf, manifest_len, &pos, output_path, MAX_LINE_LEN);
|
||||||
|
if (len <= 0) {
|
||||||
|
printf("ERROR: manifest has unpaired entry for '%s'\n", input_path);
|
||||||
|
error_count++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compile */
|
||||||
|
if (compile_file(input_path, output_path, source_buf, output_buf) != 0) {
|
||||||
|
error_count++;
|
||||||
|
break; /* stop on first error */
|
||||||
|
}
|
||||||
|
file_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clean up */
|
||||||
|
psyqo_free(source_buf);
|
||||||
|
psyqo_free(output_buf);
|
||||||
|
psyqo_free(manifest_buf);
|
||||||
|
|
||||||
|
/* Write sentinel */
|
||||||
|
if (error_count > 0) {
|
||||||
|
printf("FAILED: %d file(s) compiled, %d error(s)\n", file_count, error_count);
|
||||||
|
write_sentinel("ERROR");
|
||||||
|
} else {
|
||||||
|
printf("SUCCESS: %d file(s) compiled\n", file_count);
|
||||||
|
write_sentinel("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Halt - PCSX-Redux will kill us */
|
||||||
|
while (1) {}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
99
tools/luac_psx/pcdrv.h
Normal file
99
tools/luac_psx/pcdrv.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 PCSX-Redux authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
static inline int PCinit() {
|
||||||
|
register int r asm("v0");
|
||||||
|
__asm__ volatile("break 0, 0x101\n" : "=r"(r));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int PCcreat(const char *name, int perms) {
|
||||||
|
register const char *a0 asm("a0") = name;
|
||||||
|
register const char *a1 asm("a1") = name;
|
||||||
|
register int a2 asm("a2") = 0;
|
||||||
|
register int v0 asm("v0");
|
||||||
|
register int v1 asm("v1");
|
||||||
|
__asm__ volatile("break 0, 0x102\n" : "=r"(v0), "=r"(v1) : "r"(a0), "r"(a1), "r"(a2));
|
||||||
|
if (v0 == 0) return v1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int PCopen(const char *name, int flags, int perms) {
|
||||||
|
register int a2 asm("a2") = flags;
|
||||||
|
register const char *a0 asm("a0") = name;
|
||||||
|
register const char *a1 asm("a1") = name;
|
||||||
|
register int v0 asm("v0");
|
||||||
|
register int v1 asm("v1");
|
||||||
|
__asm__ volatile("break 0, 0x103\n" : "=r"(v0), "=r"(v1) : "r"(a0), "r"(a1), "r"(a2));
|
||||||
|
if (v0 == 0) return v1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int PCclose(int fd) {
|
||||||
|
register int a0 asm("a0") = fd;
|
||||||
|
register int a1 asm("a1") = fd;
|
||||||
|
register int v0 asm("v0");
|
||||||
|
__asm__ volatile("break 0, 0x104\n" : "=r"(v0) : "r"(a0), "r"(a1) : "v1");
|
||||||
|
return v0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int PCread(int fd, void *buf, int len) {
|
||||||
|
register int a0 asm("a0") = 0;
|
||||||
|
register int a1 asm("a1") = fd;
|
||||||
|
register int a2 asm("a2") = len;
|
||||||
|
register void *a3 asm("a3") = buf;
|
||||||
|
register int v0 asm("v0");
|
||||||
|
register int v1 asm("v1");
|
||||||
|
__asm__ volatile("break 0, 0x105\n" : "=r"(v0), "=r"(v1) : "r"(a0), "r"(a1), "r"(a2), "r"(a3) : "memory");
|
||||||
|
if (v0 == 0) return v1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int PCwrite(int fd, const void *buf, int len) {
|
||||||
|
register int a0 asm("a0") = 0;
|
||||||
|
register int a1 asm("a1") = fd;
|
||||||
|
register int a2 asm("a2") = len;
|
||||||
|
register const void *a3 asm("a3") = buf;
|
||||||
|
register int v0 asm("v0");
|
||||||
|
register int v1 asm("v1");
|
||||||
|
__asm__ volatile("break 0, 0x106\n" : "=r"(v0), "=r"(v1) : "r"(a0), "r"(a1), "r"(a2), "r"(a3));
|
||||||
|
if (v0 == 0) return v1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int PClseek(int fd, int offset, int wheel) {
|
||||||
|
register int a3 asm("a3") = wheel;
|
||||||
|
register int a2 asm("a2") = offset;
|
||||||
|
register int a0 asm("a0") = fd;
|
||||||
|
register int a1 asm("a1") = fd;
|
||||||
|
register int v0 asm("v0");
|
||||||
|
register int v1 asm("v1");
|
||||||
|
__asm__ volatile("break 0, 0x107\n" : "=r"(v0), "=r"(v1) : "r"(a0), "r"(a1), "r"(a2), "r"(a3));
|
||||||
|
if (v0 == 0) return v1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user