Compare commits

33 Commits
main ... lua

Author SHA1 Message Date
Jan Racek
428725d9ce animz 2026-03-29 18:41:58 +02:00
Jan Racek
9a82f9e3a1 ITS BROKEN 2026-03-29 16:14:15 +02:00
7c344e2e37 Fixed collisions on inactive objects 2026-03-29 14:02:28 +02:00
7425d9a5b2 Fixed psyqo-lua problems 2026-03-29 12:47:05 +02:00
92d1a73248 fixup 2026-03-29 11:05:16 +02:00
Jan Racek
9f3918c8c8 sync 2026-03-29 09:35:49 +02:00
Jan Racek
561ee9dd64 Camera API, improved cutscene API 2026-03-28 20:13:12 +01:00
Jan Racek
68cf8a7460 Lua bytecode builder, cdrom fixes 2026-03-28 19:41:37 +01:00
69aa4e079d Back color configurable, added fps counter checkbox 2026-03-28 13:32:26 +01:00
bb93ecdc5d Clear screen on game start 2026-03-28 11:52:15 +01:00
Jan Racek
b01b72751a more fixes 2026-03-28 01:32:07 +01:00
Jan Racek
bfab154547 cleanup 2026-03-27 21:29:01 +01:00
Jan Racek
85eb7e59d9 memory reports 2026-03-27 19:30:01 +01:00
Jan Racek
4ff7ecdd57 bugfixes 2026-03-27 18:31:54 +01:00
Jan Racek
480323f5b9 Revamped collision system 2026-03-27 16:39:10 +01:00
Jan Racek
090402f71a Broken runtime 2026-03-27 13:46:42 +01:00
Jan Racek
eacbf4de46 Fixed audio stuff 2026-03-26 20:26:51 +01:00
Jan Racek
19bb2254f3 Broken UI and Loading screens 2026-03-26 19:14:37 +01:00
Jan Racek
37ba4c85fe Somewhat fixed ui 2026-03-25 17:14:08 +01:00
Jan Racek
f485ec36a8 broken ui system 2026-03-25 12:25:29 +01:00
Jan Racek
60a7063a17 cutscene system 2026-03-24 15:51:04 +01:00
Jan Racek
e51c06b012 hush 2026-03-24 13:01:47 +01:00
55c1d2c39b feature: Added profiler which outputs to tty, removed subdivision entirely - to be remade later 2025-09-04 18:04:30 +02:00
8151f3864c Added ways to set/clear the active flag on gameobjects from Lua 2025-08-21 21:26:13 +02:00
8bc15db7de Fixed lua function resolution, reverted fixed point handling 2025-08-20 15:32:44 +02:00
5b761ab5bc Lua function wrappers with typestring 2025-08-20 01:53:35 +02:00
09c5ad57b3 Added correct compile_flags.txt for clangd to work 2025-08-19 22:44:44 +02:00
7e4532d846 Added IsActive flags and scene Lua files 2025-04-17 15:35:59 +02:00
b5b0ae464c Created and implemented scenemanager, include cleanup, fixed winbuild? 2025-04-14 23:53:31 +02:00
f8ab161270 Bumped nugget version 2025-04-12 17:42:34 +02:00
8699ea7845 It is now possible to move gameobjects with Lua 2025-04-11 23:56:13 +02:00
441acbb6c9 Added basic lua wrapper, currently can't retrieve from registry 2025-04-11 16:21:41 +02:00
f79b69de0a Aded initial lua setup 2025-04-10 22:51:39 +02:00
68 changed files with 13492 additions and 641 deletions

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "third_party/nugget"]
path = third_party/nugget
url = https://github.com/pcsx-redux/nugget.git
url = https://github.com/pcsx-redux/nugget

View File

@@ -7,10 +7,61 @@ src/renderer.cpp \
src/splashpack.cpp \
src/camera.cpp \
src/gtemath.cpp \
src/navmesh.cpp \
output.o
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
%.o: %.bin
$(PREFIX)-objcopy -I binary --set-section-alignment .data=4 --rename-section .data=.rodata,alloc,load,readonly,data,contents -O $(FORMAT) -B mips $< $@
# Redirect Lua's allocator through our OOM-guarded wrapper
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

Binary file not shown.

297
src/animation.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -6,7 +6,6 @@
#include <psyqo/trigonometry.hh>
psxsplash::Camera::Camera() {
// Load identity
m_rotationMatrix = psyqo::SoftMath::generateRotationMatrix33(0, psyqo::SoftMath::Axis::X, m_trig);
}
@@ -23,15 +22,80 @@ void psxsplash::Camera::SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<1
}
void psxsplash::Camera::SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z) {
m_angleX = (int16_t)x.value;
m_angleY = (int16_t)y.value;
m_angleZ = (int16_t)z.value;
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 rotZ = psyqo::SoftMath::generateRotationMatrix33(z, psyqo::SoftMath::Axis::Z, m_trig);
// XYZ multiplication order (matches C#)
psyqo::SoftMath::multiplyMatrix33(rotY, rotX, &rotY);
psyqo::SoftMath::multiplyMatrix33(rotY, rotZ, &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);
}

View File

@@ -4,26 +4,34 @@
#include <psyqo/matrix.hh>
#include <psyqo/trigonometry.hh>
#include "bvh.hh"
namespace psxsplash {
// Camera class for managing 3D position and rotation.
class Camera {
public:
Camera();
void MoveX(psyqo::FixedPoint<12> x);
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);
psyqo::Vec3& GetPosition() { return m_position; }
void SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z);
psyqo::Matrix33& GetRotation();
void ExtractFrustum(Frustum& frustum) const;
int16_t GetAngleX() const { return m_angleX; }
int16_t GetAngleY() const { return m_angleY; }
int16_t GetAngleZ() const { return m_angleZ; }
private:
psyqo::Matrix33 m_rotationMatrix;
psyqo::Trig<> m_trig;
psyqo::Vec3 m_position;
int16_t m_angleX = 0, m_angleY = 0, m_angleZ = 0;
};
} // namespace psxsplash

89
src/cdromhelper.hh Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
};

View File

@@ -1,6 +1,6 @@
#pragma once
#include <cstdint>
#include <common/util/bitfield.hh>
#include <psyqo/matrix.hh>
#include <psyqo/vector.hh>
@@ -8,7 +8,24 @@
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 {
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:
union {
Tri *polygons;
@@ -16,8 +33,43 @@ class GameObject final {
};
psyqo::Vec3 position;
psyqo::Matrix33 rotation;
// Mesh data
uint16_t polyCount;
uint16_t reserved;
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) == 56, "GameObject is not 56 bytes");
static_assert(sizeof(GameObject) == 92, "GameObject is not 92 bytes");
} // namespace psxsplash

141
src/gameobject_bytecode.h Normal file
View 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,
};

View File

@@ -6,29 +6,31 @@
using namespace psyqo::GTE;
void psxsplash::MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result) {
writeSafe<PseudoRegister::Rotation>(matA);
// 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;
psyqo::GTE::writeSafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].y, matB.vs[1].y, matB.vs[2].y});
// 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;
psyqo::GTE::writeSafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].z, matB.vs[1].z, matB.vs[2].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;

View File

@@ -1,6 +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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

343
src/luaapi.hh Normal file
View 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

View File

@@ -1,61 +1,42 @@
#include <stdint.h>
#include <psyqo/advancedpad.hh>
#include <psyqo/application.hh>
#include <psyqo/fixed-point.hh>
#include <psyqo/font.hh>
#include <psyqo/gpu.hh>
#include <psyqo/scene.hh>
#include <psyqo/task.hh>
#include <psyqo/trigonometry.hh>
#include "EASTL/algorithm.h"
#include "camera.hh"
#include "navmesh.hh"
#include "psyqo/vector.hh"
#include "renderer.hh"
#include "splashpack.hh"
#include "scenemanager.hh"
#include "fileloader.hh"
// Data from the splashpack
extern uint8_t _binary_output_bin_start[];
#if defined(LOADER_CDROM)
#include "fileloader_cdrom.hh"
#endif
namespace {
using namespace psyqo::fixed_point_literals;
using namespace psyqo::trig_literals;
class PSXSplash final : public psyqo::Application {
void prepare() override;
void createScene() override;
public:
psxsplash::SplashPackLoader m_loader;
psyqo::Font<> m_font;
psyqo::AdvancedPad m_input;
static constexpr uint8_t m_stickDeadzone = 0x30;
};
class MainScene final : public psyqo::Scene {
void frame() override;
void start(StartReason reason) override;
psxsplash::Camera m_mainCamera;
psyqo::Angle camRotX, camRotY, camRotZ;
psyqo::Trig<> m_trig;
uint32_t m_lastFrameCounter;
static constexpr psyqo::FixedPoint<12> moveSpeed = 0.002_fp;
static constexpr psyqo::Angle rotSpeed = 0.01_pi;
psxsplash::SceneManager m_sceneManager;
bool m_sprinting = false;
static constexpr psyqo::FixedPoint<12> sprintSpeed = 0.01_fp;
bool m_freecam = false;
psyqo::FixedPoint<12> pheight = 0.0_fp;
bool m_renderSelect = false;
// 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 app;
@@ -73,43 +54,58 @@ void PSXSplash::prepare() {
// 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() {
m_font.uploadSystemFont(gpu());
m_input.initialize();
psxsplash::SceneManager::SetFont(&m_font);
pushScene(&mainScene);
}
void MainScene::start(StartReason reason) {
app.m_loader.LoadSplashpack(_binary_output_bin_start);
psxsplash::Renderer::GetInstance().SetCamera(m_mainCamera);
// Initialise the FileLoader backend, then load scene 0 through
// 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_mainCamera.SetPosition(static_cast<psyqo::FixedPoint<12>>(app.m_loader.playerStartPos.x),
static_cast<psyqo::FixedPoint<12>>(app.m_loader.playerStartPos.y),
static_cast<psyqo::FixedPoint<12>>(app.m_loader.playerStartPos.z));
pheight = psyqo::FixedPoint<12>(app.m_loader.playerHeight);
app.m_input.setOnEvent(
eastl::function<void(psyqo::AdvancedPad::Event)>{[this](const psyqo::AdvancedPad::Event& event) {
if (event.pad != psyqo::AdvancedPad::Pad::Pad1a) return;
if (app.m_loader.navmeshes.empty()) return;
if (event.type == psyqo::AdvancedPad::Event::ButtonPressed) {
if (event.button == psyqo::AdvancedPad::Button::Triangle) {
m_freecam = !m_freecam;
} else if (event.button == psyqo::AdvancedPad::Button::Square) {
m_renderSelect = !m_renderSelect;
}
}
}});
if (app.m_loader.navmeshes.empty()) {
m_freecam = true;
}
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() {
// 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();
auto currentFrameCounter = gpu().getFrameCount();
auto deltaTime = currentFrameCounter - mainScene.m_lastFrameCounter;
@@ -120,74 +116,15 @@ void MainScene::frame() {
}
mainScene.m_lastFrameCounter = currentFrameCounter;
uint8_t rightX = app.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 0);
uint8_t rightY = app.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 1);
uint8_t leftX = app.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 2);
uint8_t leftY = app.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 3);
int16_t rightXOffset = (int16_t)rightX - 0x80;
int16_t rightYOffset = (int16_t)rightY - 0x80;
int16_t leftXOffset = (int16_t)leftX - 0x80;
int16_t leftYOffset = (int16_t)leftY - 0x80;
if (__builtin_abs(leftXOffset) < app.m_stickDeadzone &&
__builtin_abs(leftYOffset) < app.m_stickDeadzone) {
m_sprinting = false;
}
if (app.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L3)) {
m_sprinting = true;
}
psyqo::FixedPoint<12> speed = m_sprinting ? sprintSpeed : moveSpeed;
if (__builtin_abs(rightXOffset) > app.m_stickDeadzone) {
camRotY += (rightXOffset * rotSpeed * deltaTime) >> 7;
}
if (__builtin_abs(rightYOffset) > app.m_stickDeadzone) {
camRotX -= (rightYOffset * rotSpeed * deltaTime) >> 7;
camRotX = eastl::clamp(camRotX, -0.5_pi, 0.5_pi);
}
m_mainCamera.SetRotation(camRotX, camRotY, camRotZ);
if (__builtin_abs(leftYOffset) > app.m_stickDeadzone) {
psyqo::FixedPoint<12> forward = -(leftYOffset * speed * deltaTime) >> 7;
m_mainCamera.MoveX((m_trig.sin(camRotY) * forward));
m_mainCamera.MoveZ((m_trig.cos(camRotY) * forward));
}
if (__builtin_abs(leftXOffset) > app.m_stickDeadzone) {
psyqo::FixedPoint<12> strafe = -(leftXOffset * speed * deltaTime) >> 7;
m_mainCamera.MoveX(-(m_trig.cos(camRotY) * strafe));
m_mainCamera.MoveZ((m_trig.sin(camRotY) * strafe));
}
if (app.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1)) {
m_mainCamera.MoveY(speed * deltaTime);
}
if (app.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1)) {
m_mainCamera.MoveY(-speed * deltaTime);
}
if (!m_freecam) {
psyqo::Vec3 adjustedPosition =
psxsplash::ComputeNavmeshPosition(m_mainCamera.GetPosition(), *app.m_loader.navmeshes[0], -pheight);
m_mainCamera.SetPosition(adjustedPosition.x, adjustedPosition.y, adjustedPosition.z);
}
if (!m_renderSelect) {
psxsplash::Renderer::GetInstance().Render(app.m_loader.gameObjects);
} else {
psxsplash::Renderer::GetInstance().RenderNavmeshPreview(*app.m_loader.navmeshes[0], true);
}
m_sceneManager.GameTick(gpu());
#if defined(PSXSPLASH_FPSOVERLAY)
app.m_font.chainprintf(gpu(), {{.x = 2, .y = 2}}, {{.r = 0xff, .g = 0xff, .b = 0xff}}, "FPS: %i",
gpu().getRefreshRate() / deltaTime);
gpu().getRefreshRate() / deltaTime);
#endif
gpu().pumpCallbacks();
uint32_t endFrame = gpu().now();
uint32_t spent = endFrame - beginFrame;
}
int main() { return app.run(); }

96
src/memoverlay.cpp Normal file
View 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
View 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

View File

@@ -5,6 +5,9 @@
namespace psxsplash {
// Sentinel value for untextured (vertex-color-only) triangles
static constexpr uint16_t UNTEXTURED_TPAGE = 0xFFFF;
class Tri final {
public:
psyqo::GTE::PackedVec3 v0, v1, v2;
@@ -18,7 +21,12 @@ namespace psxsplash {
psyqo::PrimPieces::TPageAttr tpage;
uint16_t clutX;
uint16_t clutY;
uint16_t padding;
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");

View File

@@ -1,122 +0,0 @@
#include "navmesh.hh"
#include <array>
#include "psyqo/fixed-point.hh"
#include "psyqo/vector.hh"
using namespace psyqo::fixed_point_literals;
// FIXME: This entire file uses hard FixedPoint scaling of 100. This is not ideal.
// It would be better to move the fixedpoint precision to 19 instead.
namespace psxsplash {
psyqo::FixedPoint<12> DotProduct2D(const psyqo::Vec2& a, const psyqo::Vec2& b) { return a.x * b.x + a.y * b.y; }
psyqo::Vec2 ClosestPointOnSegment(const psyqo::Vec2& A, const psyqo::Vec2& B, const psyqo::Vec2& P) {
psyqo::Vec2 AB = {B.x - A.x, B.y - A.y};
psyqo::Vec2 AP = {P.x - A.x, P.y - A.y};
auto abDot = DotProduct2D(AB, AB);
if (abDot == 0) return A;
psyqo::FixedPoint<12> t = DotProduct2D(AP, AB) / abDot;
if (t < 0.0_fp) t = 0.0_fp;
if (t > 1.0_fp) t = 1.0_fp;
return {(A.x + AB.x * t), (A.y + AB.y * t)};
}
bool PointInTriangle(psyqo::Vec3& p, NavMeshTri& tri) {
psyqo::Vec2 A = {tri.v0.x * 100, tri.v0.z * 100};
psyqo::Vec2 B = {tri.v1.x * 100, tri.v1.z * 100};
psyqo::Vec2 C = {tri.v2.x * 100, tri.v2.z * 100};
psyqo::Vec2 P = {p.x * 100, p.z * 100};
psyqo::Vec2 v0 = {B.x - A.x, B.y - A.y};
psyqo::Vec2 v1 = {C.x - A.x, C.y - A.y};
psyqo::Vec2 v2 = {P.x - A.x, P.y - A.y};
auto d00 = DotProduct2D(v0, v0);
auto d01 = DotProduct2D(v0, v1);
auto d11 = DotProduct2D(v1, v1);
auto d20 = DotProduct2D(v2, v0);
auto d21 = DotProduct2D(v2, v1);
psyqo::FixedPoint<12> denom = d00 * d11 - d01 * d01;
if (denom == 0.0_fp) {
return false;
}
auto invDenom = 1.0_fp / denom;
auto u = (d11 * d20 - d01 * d21) * invDenom;
auto w = (d00 * d21 - d01 * d20) * invDenom;
return (u >= 0.0_fp) && (w >= 0.0_fp) && (u + w <= 1.0_fp);
}
psyqo::Vec3 ComputeNormal(const NavMeshTri& tri) {
psyqo::Vec3 v1 = {tri.v1.x * 100 - tri.v0.x * 100, tri.v1.y * 100 - tri.v0.y * 100, tri.v1.z * 100 - tri.v0.z * 100};
psyqo::Vec3 v2 = {tri.v2.x * 100 - tri.v0.x * 100, tri.v2.y * 100 - tri.v0.y * 100, tri.v2.z * 100 - tri.v0.z * 100};
psyqo::Vec3 normal = {
v1.y * v2.z - v1.z * v2.y,
v1.z * v2.x - v1.x * v2.z,
v1.x * v2.y - v1.y * v2.x
};
return normal;
}
psyqo::FixedPoint<12> CalculateY(const psyqo::Vec3& p, const NavMeshTri& tri) {
psyqo::Vec3 normal = ComputeNormal(tri);
psyqo::FixedPoint<12> A = normal.x;
psyqo::FixedPoint<12> B = normal.y;
psyqo::FixedPoint<12> C = normal.z;
psyqo::FixedPoint<12> D = -(A * tri.v0.x + B * tri.v0.y + C * tri.v0.z);
if (B != 0.0_fp) {
return -(A * p.x + C * p.z + D) / B;
} else {
return p.y;
}
}
psyqo::Vec3 ComputeNavmeshPosition(psyqo::Vec3& position, Navmesh& navmesh, psyqo::FixedPoint<12> pheight) {
for (int i = 0; i < navmesh.triangleCount; i++) {
if (PointInTriangle(position, navmesh.polygons[i])) {
position.y = CalculateY(position, navmesh.polygons[i]) + pheight;
return position;
}
}
psyqo::Vec2 P = {position.x * 100, position.z * 100};
psyqo::Vec2 closestPoint;
psyqo::FixedPoint<12> minDist = 0x7ffff;
for (int i = 0; i < navmesh.triangleCount; i++) {
NavMeshTri& tri = navmesh.polygons[i];
psyqo::Vec2 A = {tri.v0.x * 100, tri.v0.z * 100};
psyqo::Vec2 B = {tri.v1.x * 100, tri.v1.z * 100};
psyqo::Vec2 C = {tri.v2.x * 100, tri.v2.z * 100};
std::array<std::pair<psyqo::Vec2, psyqo::Vec2>, 3> edges = {{{A, B}, {B, C}, {C, A}}};
for (auto& edge : edges) {
psyqo::Vec2 proj = ClosestPointOnSegment(edge.first, edge.second, P);
psyqo::Vec2 diff = {proj.x - P.x, proj.y - P.y};
auto distSq = DotProduct2D(diff, diff);
if (distSq < minDist) {
minDist = distSq;
closestPoint = proj;
position.y = CalculateY(position, navmesh.polygons[i]) + pheight;
}
}
}
position.x = closestPoint.x / 100;
position.z = closestPoint.y / 100;
return position;
}
} // namespace psxsplash

View File

@@ -1,24 +0,0 @@
#pragma once
#include <psyqo/vector.hh>
namespace psxsplash {
class NavMeshTri final {
public:
psyqo::Vec3 v0, v1, v2;
};
class Navmesh final {
public:
union {
NavMeshTri* polygons;
uint32_t polygonsOffset;
};
uint16_t triangleCount;
uint16_t reserved;
};
psyqo::Vec3 ComputeNavmeshPosition(psyqo::Vec3& position, Navmesh& navmesh, psyqo::FixedPoint<12> pheight);
} // namespace psxsplash

291
src/navregion.cpp Normal file
View 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
View 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
View 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
View 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(&sectionTimes[0], "profiler.rendering");
pcsxRegisterVariable(&sectionTimes[1], "profiler.lua");
pcsxRegisterVariable(&sectionTimes[2], "profiler.controls");
pcsxRegisterVariable(&sectionTimes[3], "profiler.navmesh");
}
void Profiler::reset() {
for (auto &time : sectionTimes) {
time = 0;
}
}
#endif // PSXSPLASH_PROFILER

47
src/profiler.hh Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -13,33 +13,68 @@
#include <psyqo/primitives/triangles.hh>
#include <psyqo/trigonometry.hh>
#include "bvh.hh"
#include "camera.hh"
#include "gameobject.hh"
#include "navmesh.hh"
#include "triclip.hh"
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 {
public:
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
// FIXME: I have no idea how to precompute the required sizes of these. It would be best to allocate them based on the scene
static constexpr size_t ORDERING_TABLE_SIZE = 2048 * 3;
static constexpr size_t BUMP_ALLOCATOR_SIZE = 8096 * 24;
#ifndef OT_SIZE
#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 constexpr int32_t PROJ_H = 120;
static constexpr int32_t SCREEN_CX = 160;
static constexpr int32_t SCREEN_CY = 120;
static void Init(psyqo::GPU& gpuInstance);
void SetCamera(Camera& camera);
void SetFog(const FogConfig& fog);
void Render(eastl::vector<GameObject*>& objects);
void RenderNavmeshPreview(psxsplash::Navmesh navmesh, bool isOnMesh);
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);
void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY, int16_t width, int16_t height);
void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY,
int16_t width, int16_t height);
void SetUISystem(UISystem* ui) { m_uiSystem = ui; }
#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");
psyqo::Kernel::assert(instance != nullptr,
"Access to renderer was tried without prior initialization");
return *instance;
}
@@ -49,8 +84,7 @@ class Renderer final {
Renderer(psyqo::GPU& gpuInstance) : m_gpu(gpuInstance) {}
~Renderer() {}
Camera* m_currentCamera;
Camera* m_currentCamera = nullptr;
psyqo::GPU& m_gpu;
psyqo::Trig<> m_trig;
@@ -58,10 +92,23 @@ class Renderer final {
psyqo::Fragments::SimpleFragment<psyqo::Prim::FastFill> m_clear[2];
psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE> m_ballocs[2];
FogConfig m_fog;
psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0};
void recursiveSubdivideAndRender(Tri &tri, eastl::array<psyqo::Vertex, 3> &projected, int zIndex,
int maxIterations);
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
View 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
View 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

View File

@@ -1,12 +1,19 @@
#include "splashpack.hh"
#include <cstring>
#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 "cutscene.hh"
#include "lua.h"
#include "mesh.hh"
#include "psyqo/fixed-point.hh"
#include "psyqo/gte-registers.hh"
#include "streq.hh"
#include "navregion.hh"
#include "renderer.hh"
namespace psxsplash {
@@ -14,15 +21,54 @@ namespace psxsplash {
struct SPLASHPACKFileHeader {
char magic[2];
uint16_t version;
uint16_t luaFileCount;
uint16_t gameObjectCount;
uint16_t navmeshCount;
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 pad[1];
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;
@@ -38,49 +84,369 @@ struct SPLASHPACKClut {
uint16_t pad;
};
void SplashPackLoader::LoadSplashpack(uint8_t *data) {
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(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");
playerStartPos = header->playerStartPos;
playerStartRot = header->playerStartRot;
playerHeight = header->playerHeight;
setup.playerStartPosition = header->playerStartPos;
setup.playerStartRotation = header->playerStartRot;
setup.playerHeight = header->playerHeight;
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;
gameObjects.reserve(header->gameObjectCount);
navmeshes.reserve(header->navmeshCount);
setup.luaFiles.reserve(header->luaFileCount);
setup.objects.reserve(header->gameObjectCount);
setup.colliders.reserve(header->colliderCount);
setup.interactables.reserve(header->interactableCount);
uint8_t *curentPointer = data + sizeof(psxsplash::SPLASHPACKFileHeader);
uint8_t *cursor = data + sizeof(SPLASHPACKFileHeader);
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
psxsplash::GameObject *go = reinterpret_cast<psxsplash::GameObject *>(curentPointer);
go->polygons = reinterpret_cast<psxsplash::Tri *>(data + go->polygonsOffset);
gameObjects.push_back(go);
curentPointer += sizeof(psxsplash::GameObject);
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);
}
for (uint16_t i = 0; i < header->navmeshCount; i++) {
psxsplash::Navmesh *navmesh = reinterpret_cast<psxsplash::Navmesh *>(curentPointer);
navmesh->polygons = reinterpret_cast<psxsplash::NavMeshTri *>(data + navmesh->polygonsOffset);
navmeshes.push_back(navmesh);
curentPointer += sizeof(psxsplash::Navmesh);
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);
}
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);
}
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++) {
psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(curentPointer);
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);
curentPointer += sizeof(psxsplash::SPLASHPACKTextureAtlas);
// Ensure 4-byte alignment for DMA transfer. If the exporter
// produced an unaligned offset, copy to an aligned temporary.
uint32_t pixelBytes = (uint32_t)atlas->width * atlas->height * 2;
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);
}
for (uint16_t i = 0; i < header->clutCount; i++) {
psxsplash::SPLASHPACKClut *clut = reinterpret_cast<psxsplash::SPLASHPACKClut *>(curentPointer);
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);
curentPointer += sizeof(psxsplash::SPLASHPACKClut);
// 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
View 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

View File

@@ -2,21 +2,113 @@
#include <EASTL/vector.h>
#include <psyqo/fixed-point.hh>
#include "bvh.hh"
#include "collision.hh"
#include "gameobject.hh"
#include "navmesh.hh"
#include "psyqo/fixed-point.hh"
#include "lua.h"
#include "navregion.hh"
#include "audiomanager.hh"
#include "interactable.hh"
#include "cutscene.hh"
#include "animation.hh"
#include "uisystem.hh"
namespace psxsplash {
class SplashPackLoader {
public:
eastl::vector<GameObject *> gameObjects;
eastl::vector<Navmesh *> navmeshes;
psyqo::GTE::PackedVec3 playerStartPos, playerStartRot;
/**
* Collision data as stored in the binary file (fixed layout for serialization)
*/
struct SPLASHPACKCollider {
// 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;
void LoadSplashpack(uint8_t *data);
// 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;
};
}; // namespace psxsplash
class SplashPackLoader {
public:
void LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup);
};
} // namespace psxsplash

10
src/streq.hh Normal file
View 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

197
src/triclip.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

34
tools/luac_psx/Makefile Normal file
View 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
View 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
View 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;
}