Compare commits

...

21 Commits

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
67 changed files with 6669 additions and 2203 deletions

View File

@@ -1 +0,0 @@
ˆ¨platform¦native§release¯pcsx-redux@head«environmentªproduction¥level¥error£sdk„¤name­sentry.native§version¥0.6.1¨packages¤name¾github:getsentry/sentry-native§version¥0.6.1¬integrations¨crashpad¤tags€¥extra€¨contexts<EFBFBD>¢os„¤name§Windows®kernel_version¯10.0.26100.8036§versionª10.0.26200¥build¤8037

Binary file not shown.

View File

@@ -13,14 +13,55 @@ src/triclip.cpp \
src/lua.cpp \ src/lua.cpp \
src/luaapi.cpp \ src/luaapi.cpp \
src/scenemanager.cpp \ src/scenemanager.cpp \
src/sceneloader.cpp \ src/fileloader.cpp \
src/audiomanager.cpp \ src/audiomanager.cpp \
src/controls.cpp \ src/controls.cpp \
src/profiler.cpp \ src/profiler.cpp \
src/collision.cpp \ src/collision.cpp \
src/bvh.cpp src/bvh.cpp \
src/cutscene.cpp \
src/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 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-lua/psyqo-lua.mk
include third_party/nugget/psyqo/psyqo.mk include third_party/nugget/psyqo/psyqo.mk
# 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.

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

View File

@@ -1,199 +1,183 @@
#include "audiomanager.hh" #include "audiomanager.hh"
#include "common/hardware/dma.h"
#include "common/hardware/spu.h" #include "common/hardware/spu.h"
#include <psyqo/kernel.hh>
#include <psyqo/spu.hh> #include <psyqo/spu.hh>
#include <psyqo/xprintf.h> #include <psyqo/xprintf.h>
namespace psxsplash { namespace psxsplash {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
uint16_t AudioManager::volToHw(int v) { uint16_t AudioManager::volToHw(int v) {
if (v <= 0) return 0; if (v <= 0)
if (v >= 128) return 0x3fff; return 0;
return static_cast<uint16_t>((v * 0x3fff) / 128); if (v >= 128)
return 0x3fff;
return static_cast<uint16_t>((v * 0x3fff) / 128);
} }
// ---------------------------------------------------------------------------
// Init / Reset
// ---------------------------------------------------------------------------
void AudioManager::init() { void AudioManager::init() {
psyqo::SPU::initialize(); psyqo::SPU::initialize();
m_nextAddr = SPU_RAM_START; m_nextAddr = SPU_RAM_START;
for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { for (int i = 0; i < MAX_AUDIO_CLIPS; i++) {
m_clips[i].loaded = false; m_clips[i].loaded = false;
} }
} }
void AudioManager::reset() { void AudioManager::reset() {
stopAll(); stopAll();
for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { for (int i = 0; i < MAX_AUDIO_CLIPS; i++) {
m_clips[i].loaded = false; m_clips[i].loaded = false;
} }
m_nextAddr = SPU_RAM_START; m_nextAddr = SPU_RAM_START;
} }
// --------------------------------------------------------------------------- bool AudioManager::loadClip(int clipIndex, const uint8_t *adpcmData,
// Clip loading uint32_t sizeBytes, uint16_t sampleRate,
// --------------------------------------------------------------------------- bool loop) {
if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS)
return false;
if (!adpcmData || sizeBytes == 0)
return false;
bool AudioManager::loadClip(int clipIndex, const uint8_t* adpcmData, uint32_t sizeBytes, // check for and skip VAG header if present
uint16_t sampleRate, bool loop) { if (sizeBytes >= 48) {
if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS) return false; const char *magic = reinterpret_cast<const char *>(adpcmData);
if (!adpcmData || sizeBytes == 0) return false; if (magic[0] == 'V' && magic[1] == 'A' && magic[2] == 'G' &&
magic[3] == 'p') {
adpcmData += 48;
sizeBytes -= 48;
}
}
// Check for VAG header (magic "VAGp" at offset 0). uint32_t addr = (m_nextAddr + 15) & ~15u;
// If present, the header wasn't stripped properly — skip it. uint32_t alignedSize = (sizeBytes + 15) & ~15u;
if (sizeBytes >= 48) {
const char* magic = reinterpret_cast<const char*>(adpcmData); if (addr + alignedSize > SPU_RAM_END) {
if (magic[0] == 'V' && magic[1] == 'A' && magic[2] == 'G' && magic[3] == 'p') { return false;
adpcmData += 48; }
sizeBytes -= 48;
} 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)) {
} }
// Align to 16-byte SPU ADPCM block boundary src += bytesThisRound;
uint32_t addr = (m_nextAddr + 15) & ~15u; dstAddr += bytesThisRound;
uint32_t alignedSize = (sizeBytes + 15) & ~15u; remaining -= bytesThisRound;
}
if (addr + alignedSize > SPU_RAM_END) { SPU_CTRL &= ~(0b11 << 4);
return false;
}
// psyqo::SPU::dmaWrite takes dataSize as uint16_t so upload in chunks m_clips[clipIndex].spuAddr = addr;
// for clips larger than 65532 bytes (largest multiple-of-4 that fits). m_clips[clipIndex].size = sizeBytes;
// m_clips[clipIndex].sampleRate = sampleRate;
// psyqo DMA math: BCR = blockSize | ((dataSize/blockSize) << 16) m_clips[clipIndex].loop = loop;
// blockSize=4 → 4 words per block = 16 bytes per block m_clips[clipIndex].loaded = true;
// block count = dataSize/blockSize
// total bytes = blockSize × (dataSize/blockSize) × 4 = dataSize × 4
// So dataSize = bytesThisRound / 4 gives the correct byte count.
const uint8_t* src = adpcmData;
uint32_t remaining = alignedSize;
uint32_t dstAddr = addr;
while (remaining > 0) {
// Max transfer per call: 65532 bytes (16383 blocks × 4 bytes each).
uint32_t bytesThisRound = (remaining > 65532u) ? 65532u : remaining;
bytesThisRound &= ~3u; // DMA alignment
if (bytesThisRound == 0) break;
uint16_t dmaSizeParam = (uint16_t)(bytesThisRound / 4); m_nextAddr = addr + alignedSize;
psyqo::SPU::dmaWrite(dstAddr, src, dmaSizeParam, 4); return true;
src += bytesThisRound;
dstAddr += bytesThisRound;
remaining -= bytesThisRound;
}
// dmaWrite() now properly restores transfer mode to idle after each
// DMA transfer, so no manual SPU_CTRL fix-up is needed here.
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;
} }
// ---------------------------------------------------------------------------
// Playback
// ---------------------------------------------------------------------------
int AudioManager::play(int clipIndex, int volume, int pan) { int AudioManager::play(int clipIndex, int volume, int pan) {
if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS || !m_clips[clipIndex].loaded) { if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS ||
return -1; !m_clips[clipIndex].loaded) {
} return -1;
}
uint32_t ch = psyqo::SPU::getNextFreeChannel(); uint32_t ch = psyqo::SPU::getNextFreeChannel();
if (ch == psyqo::SPU::NO_FREE_CHANNEL) return -1; if (ch == psyqo::SPU::NO_FREE_CHANNEL)
return -1;
const AudioClip& clip = m_clips[clipIndex]; const AudioClip &clip = m_clips[clipIndex];
uint16_t vol = volToHw(volume); uint16_t vol = volToHw(volume);
uint16_t leftVol = vol; uint16_t leftVol = vol;
uint16_t rightVol = vol; uint16_t rightVol = vol;
if (pan != 64) { if (pan != 64) {
int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan); int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan);
leftVol = (uint16_t)((uint32_t)vol * (127 - p) / 127); leftVol = (uint16_t)((uint32_t)vol * (127 - p) / 127);
rightVol = (uint16_t)((uint32_t)vol * p / 127); rightVol = (uint16_t)((uint32_t)vol * p / 127);
} }
psyqo::SPU::ChannelPlaybackConfig config; constexpr uint16_t DUMMY_SPU_ADDR = 0x1000;
config.sampleRate.value = static_cast<uint16_t>(((uint32_t)clip.sampleRate << 12) / 44100); if (clip.loop) {
config.volumeLeft = leftVol; SPU_VOICES[ch].sampleRepeatAddr = static_cast<uint16_t>(clip.spuAddr / 8);
config.volumeRight = rightVol; } else {
config.adsr = DEFAULT_ADSR; SPU_VOICES[ch].sampleRepeatAddr = DUMMY_SPU_ADDR / 8;
}
// Set the repeat address depending on loop mode. psyqo::SPU::ChannelPlaybackConfig config;
// The new psyqo::SPU::getNextFreeChannel() uses the ENDX register: config.sampleRate.value =
// a channel is "free" when its ENDX bit is set (voice reached loop-end). static_cast<uint16_t>(((uint32_t)clip.sampleRate << 12) / 44100);
// silenceChannels() points voices at psyqo's silent dummy sample at 0x1000 config.volumeLeft = leftVol;
// that immediately sets ENDX, so stopped channels are detected as free. config.volumeRight = rightVol;
// config.adsr = DEFAULT_ADSR;
// Looping clips: repeat → clip start (loop back to beginning).
// Non-looping clips: repeat → dummy 0x1000 (go silent after clip ends,
// dummy's loop-end flag re-sets ENDX → channel freed).
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::playADPCM(static_cast<uint8_t>(ch), if (ch > 15) {
static_cast<uint16_t>(clip.spuAddr), SPU_KEY_OFF_HIGH = 1 << (ch - 16);
config, true); } else {
SPU_KEY_OFF_LOW = 1 << ch;
}
return static_cast<int>(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);
} }
// ---------------------------------------------------------------------------
// Stop
// ---------------------------------------------------------------------------
void AudioManager::stopVoice(int channel) { void AudioManager::stopVoice(int channel) {
if (channel < 0 || channel >= MAX_VOICES) return; if (channel < 0 || channel >= MAX_VOICES)
psyqo::SPU::silenceChannels(1u << channel); return;
psyqo::SPU::silenceChannels(1u << channel);
} }
void AudioManager::stopAll() { void AudioManager::stopAll() { psyqo::SPU::silenceChannels(0x00FFFFFFu); }
psyqo::SPU::silenceChannels(0x00FFFFFFu);
}
// ---------------------------------------------------------------------------
// Volume
// ---------------------------------------------------------------------------
void AudioManager::setVoiceVolume(int channel, int volume, int pan) { void AudioManager::setVoiceVolume(int channel, int volume, int pan) {
if (channel < 0 || channel >= MAX_VOICES) return; if (channel < 0 || channel >= MAX_VOICES)
uint16_t vol = volToHw(volume); return;
if (pan == 64) { uint16_t vol = volToHw(volume);
SPU_VOICES[channel].volumeLeft = vol; if (pan == 64) {
SPU_VOICES[channel].volumeRight = vol; SPU_VOICES[channel].volumeLeft = vol;
} else { SPU_VOICES[channel].volumeRight = vol;
int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan); } else {
SPU_VOICES[channel].volumeLeft = (uint16_t)((uint32_t)vol * (127 - p) / 127); int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan);
SPU_VOICES[channel].volumeRight = (uint16_t)((uint32_t)vol * p / 127); SPU_VOICES[channel].volumeLeft =
} (uint16_t)((uint32_t)vol * (127 - p) / 127);
SPU_VOICES[channel].volumeRight = (uint16_t)((uint32_t)vol * p / 127);
}
} }
// ---------------------------------------------------------------------------
// Query
// ---------------------------------------------------------------------------
int AudioManager::getLoadedClipCount() const { int AudioManager::getLoadedClipCount() const {
int count = 0; int count = 0;
for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { for (int i = 0; i < MAX_AUDIO_CLIPS; i++) {
if (m_clips[i].loaded) count++; if (m_clips[i].loaded)
} count++;
return count; }
return count;
} }
} // namespace psxsplash } // namespace psxsplash

View File

@@ -4,44 +4,25 @@
namespace psxsplash { namespace psxsplash {
/// Maximum number of audio clips that can be loaded in a scene
static constexpr int MAX_AUDIO_CLIPS = 32; static constexpr int MAX_AUDIO_CLIPS = 32;
/// Maximum SPU voices (hardware limit)
static constexpr int MAX_VOICES = 24; static constexpr int MAX_VOICES = 24;
/// SPU RAM is 512KB total (0x00000-0x7FFFF).
/// First 0x1000 bytes reserved for capture buffers.
/// psyqo places a 16-byte silent dummy sample at 0x1000.
/// User clips start at 0x1010.
///
/// Upper bound is 0x10000 (64KB) because psyqo::SPU::playADPCM()
/// takes a uint16_t for the SPU RAM address.
static constexpr uint32_t SPU_RAM_START = 0x1010; static constexpr uint32_t SPU_RAM_START = 0x1010;
static constexpr uint32_t SPU_RAM_END = 0x10000; static constexpr uint32_t SPU_RAM_END = 0x80000;
/// Default ADSR: instant attack, sustain at max, ~46ms linear release.
/// Lower 16-bit (AD): attack linear shift=0 step=0("+7"), decay shift=0,
/// sustain level=0xF (max -> decay skipped)
/// Upper 16-bit (SR): sustain linear increase shift=0 step=0("+7"),
/// release linear shift=10 (~46ms to zero)
static constexpr uint32_t DEFAULT_ADSR = 0x000A000F; static constexpr uint32_t DEFAULT_ADSR = 0x000A000F;
/// Descriptor for a loaded audio clip in SPU RAM
struct AudioClip { struct AudioClip {
uint32_t spuAddr; // Byte address in SPU RAM uint32_t spuAddr;
uint32_t size; // Size of ADPCM data in bytes uint32_t size;
uint16_t sampleRate; // Original sample rate in Hz uint16_t sampleRate;
bool loop; // Whether this clip should loop bool loop;
bool loaded; // Whether this slot is valid bool loaded;
}; };
/// Manages SPU voices and audio clip playback. /// Manages SPU voices and audio clip playback.
/// ///
/// Uses psyqo::SPU for all hardware interaction: initialization,
/// DMA uploads, voice allocation (via currentVolume check), playback
/// (playADPCM), and silencing (silenceChannels).
///
/// init() /// init()
/// loadClip(index, data, size, rate, loop) -> bool /// loadClip(index, data, size, rate, loop) -> bool
/// play(clipIndex) -> channel /// play(clipIndex) -> channel
@@ -53,45 +34,45 @@ struct AudioClip {
/// Volume is 0-128 (0=silent, 128=max). Pan is 0-127 (64=center). /// Volume is 0-128 (0=silent, 128=max). Pan is 0-127 (64=center).
class AudioManager { class AudioManager {
public: public:
/// Initialize SPU hardware and reset state /// Initialize SPU hardware and reset state
void init(); void init();
/// Upload ADPCM data to SPU RAM and register as clip index. /// Upload ADPCM data to SPU RAM and register as clip index.
/// Data must be 16-byte aligned (SPU ADPCM block size). Returns true on success. /// Data must be 16-byte aligned. Returns true on success.
bool loadClip(int clipIndex, const uint8_t* adpcmData, uint32_t sizeBytes, bool loadClip(int clipIndex, const uint8_t *adpcmData, uint32_t sizeBytes,
uint16_t sampleRate, bool loop); uint16_t sampleRate, bool loop);
/// Play a clip by index. Returns channel (0-23), or -1 if full. /// 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. /// Volume: 0-128 (128=max). Pan: 0 (left) to 127 (right), 64 = center.
int play(int clipIndex, int volume = 128, int pan = 64); int play(int clipIndex, int volume = 128, int pan = 64);
/// Stop a specific channel (returned from play()) /// Stop a specific channel
void stopVoice(int channel); void stopVoice(int channel);
/// Stop all playing channels /// Stop all playing channels
void stopAll(); void stopAll();
/// Set volume/pan on a playing channel /// Set volume/pan on a playing channel
void setVoiceVolume(int channel, int volume, int pan = 64); void setVoiceVolume(int channel, int volume, int pan = 64);
/// Get total SPU RAM used by loaded clips (for visualization) /// Get total SPU RAM used by loaded clips
uint32_t getUsedSPURam() const { return m_nextAddr - SPU_RAM_START; } uint32_t getUsedSPURam() const { return m_nextAddr - SPU_RAM_START; }
/// Get total SPU RAM available /// Get total SPU RAM available
uint32_t getTotalSPURam() const { return SPU_RAM_END - SPU_RAM_START; } uint32_t getTotalSPURam() const { return SPU_RAM_END - SPU_RAM_START; }
/// Get number of loaded clips /// Get number of loaded clips
int getLoadedClipCount() const; int getLoadedClipCount() const;
/// Reset all clips and free SPU RAM (call on scene unload) /// Reset all clips and free SPU RAM
void reset(); void reset();
private: private:
/// Convert 0-128 volume to hardware 0-0x3FFF (fixed-volume mode) /// Convert 0-128 volume to hardware 0-0x3FFF (fixed-volume mode)
static uint16_t volToHw(int v); static uint16_t volToHw(int v);
AudioClip m_clips[MAX_AUDIO_CLIPS]; AudioClip m_clips[MAX_AUDIO_CLIPS];
uint32_t m_nextAddr = SPU_RAM_START; // Bump allocator for SPU RAM uint32_t m_nextAddr = SPU_RAM_START;
}; };
} // namespace psxsplash } // namespace psxsplash

View File

@@ -2,142 +2,123 @@
namespace psxsplash { namespace psxsplash {
void BVHManager::initialize(const BVHNode* nodes, uint16_t nodeCount, void BVHManager::initialize(const BVHNode *nodes, uint16_t nodeCount,
const TriangleRef* triangleRefs, uint16_t triangleRefCount) { const TriangleRef *triangleRefs,
m_nodes = nodes; uint16_t triangleRefCount) {
m_nodeCount = nodeCount; m_nodes = nodes;
m_triangleRefs = triangleRefs; m_nodeCount = nodeCount;
m_triangleRefCount = triangleRefCount; m_triangleRefs = triangleRefs;
m_triangleRefCount = triangleRefCount;
} }
const uint8_t* BVHManager::initializeFromData(const uint8_t* data, uint16_t nodeCount, uint16_t triangleRefCount) {
if (data == nullptr || nodeCount == 0) { int BVHManager::cullFrustum(const Frustum &frustum, TriangleRef *outRefs,
m_nodes = nullptr; int maxRefs) const {
m_triangleRefs = nullptr; if (!isLoaded() || m_nodeCount == 0)
m_nodeCount = 0; return 0;
m_triangleRefCount = 0;
return data; return traverseFrustum(0, frustum, outRefs, 0, maxRefs);
}
// Point to node array
m_nodes = reinterpret_cast<const BVHNode*>(data);
m_nodeCount = nodeCount;
data += m_nodeCount * sizeof(BVHNode);
// Point to triangle ref array
m_triangleRefs = reinterpret_cast<const TriangleRef*>(data);
m_triangleRefCount = triangleRefCount;
data += m_triangleRefCount * sizeof(TriangleRef);
return data;
} }
int BVHManager::cullFrustum(const Frustum& frustum, int BVHManager::traverseFrustum(int nodeIndex, const Frustum &frustum,
TriangleRef* outRefs, TriangleRef *outRefs, int currentCount,
int maxRefs) const { int maxRefs) const {
if (!isLoaded() || m_nodeCount == 0) return 0; if (nodeIndex < 0 || nodeIndex >= m_nodeCount)
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];
// Frustum test - if node is completely outside, skip entire subtree
if (!frustum.testAABB(node)) {
return currentCount; // Culled!
}
// If leaf, add all triangles
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;
}
// Recurse into children
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; 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, int BVHManager::queryRegion(int32_t minX, int32_t minY, int32_t minZ,
int32_t maxX, int32_t maxY, int32_t maxZ, int32_t maxX, int32_t maxY, int32_t maxZ,
TriangleRef* outRefs, TriangleRef *outRefs, int maxRefs) const {
int maxRefs) const { if (!isLoaded() || m_nodeCount == 0)
if (!isLoaded() || m_nodeCount == 0) return 0; return 0;
return traverseRegion(0, minX, minY, minZ, maxX, maxY, maxZ, outRefs, 0, maxRefs); return traverseRegion(0, minX, minY, minZ, maxX, maxY, maxZ, outRefs, 0,
maxRefs);
} }
int BVHManager::traverseRegion(int nodeIndex, int BVHManager::traverseRegion(int nodeIndex, int32_t qMinX, int32_t qMinY,
int32_t qMinX, int32_t qMinY, int32_t qMinZ, int32_t qMinZ, int32_t qMaxX, int32_t qMaxY,
int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ, int32_t qMaxZ, TriangleRef *outRefs,
TriangleRef* outRefs, int currentCount, int maxRefs) const {
int currentCount, if (nodeIndex < 0 || nodeIndex >= m_nodeCount)
int maxRefs) const {
if (nodeIndex < 0 || nodeIndex >= m_nodeCount) return currentCount;
if (currentCount >= maxRefs) return currentCount;
const BVHNode& node = m_nodes[nodeIndex];
// AABB overlap test
if (!aabbOverlap(node, qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ)) {
return currentCount; // No overlap, skip
}
// If leaf, add all triangles
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;
}
// Recurse into children
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; 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, bool BVHManager::aabbOverlap(const BVHNode &node, int32_t qMinX, int32_t qMinY,
int32_t qMinX, int32_t qMinY, int32_t qMinZ, int32_t qMinZ, int32_t qMaxX, int32_t qMaxY,
int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ) { int32_t qMaxZ) {
// Check for separation on any axis if (node.maxX < qMinX || node.minX > qMaxX)
if (node.maxX < qMinX || node.minX > qMaxX) return false; return false;
if (node.maxY < qMinY || node.minY > qMaxY) return false; if (node.maxY < qMinY || node.minY > qMaxY)
if (node.maxZ < qMinZ || node.minZ > qMaxZ) return false; return false;
return true; // Overlapping if (node.maxZ < qMinZ || node.minZ > qMaxZ)
return false;
return true;
} }
} // namespace psxsplash } // namespace psxsplash

View File

@@ -14,42 +14,25 @@ struct TriangleRef {
static_assert(sizeof(TriangleRef) == 4, "TriangleRef must be 4 bytes"); static_assert(sizeof(TriangleRef) == 4, "TriangleRef must be 4 bytes");
/// BVH Node - stored in binary file /// BVH Node - stored in binary file
/// 32 bytes per node for cache-friendly traversal
struct BVHNode { struct BVHNode {
// AABB bounds in fixed-point 20.12 format int32_t minX, minY, minZ;
int32_t minX, minY, minZ; // 12 bytes int32_t maxX, maxY, maxZ;
int32_t maxX, maxY, maxZ; // 12 bytes
// Child indices (0xFFFF = no child / leaf indicator) uint16_t leftChild;
uint16_t leftChild; // 2 bytes uint16_t rightChild;
uint16_t rightChild; // 2 bytes
// Triangle data (only valid for leaf nodes) uint16_t firstTriangle;
uint16_t firstTriangle; // 2 bytes - index into triangle ref array uint16_t triangleCount;
uint16_t triangleCount; // 2 bytes
/// Check if this is a leaf node
bool isLeaf() const { bool isLeaf() const {
return leftChild == 0xFFFF && rightChild == 0xFFFF; return leftChild == 0xFFFF && rightChild == 0xFFFF;
} }
/// Test if a point is inside this node's bounds
bool containsPoint(const psyqo::Vec3& point) const {
return point.x.raw() >= minX && point.x.raw() <= maxX &&
point.y.raw() >= minY && point.y.raw() <= maxY &&
point.z.raw() >= minZ && point.z.raw() <= maxZ;
}
/// Test if AABB intersects frustum plane
/// plane: normal (xyz) + distance (w) in fixed point
bool testPlane(int32_t nx, int32_t ny, int32_t nz, int32_t d) const { bool testPlane(int32_t nx, int32_t ny, int32_t nz, int32_t d) const {
// Find the corner most in the direction of the plane normal (p-vertex)
int32_t px = (nx >= 0) ? maxX : minX; int32_t px = (nx >= 0) ? maxX : minX;
int32_t py = (ny >= 0) ? maxY : minY; int32_t py = (ny >= 0) ? maxY : minY;
int32_t pz = (nz >= 0) ? maxZ : minZ; int32_t pz = (nz >= 0) ? maxZ : minZ;
// If p-vertex is on negative side, box is completely outside
// dot(p, n) + d < 0 means outside
int64_t dot = ((int64_t)px * nx + (int64_t)py * ny + (int64_t)pz * nz) >> 12; int64_t dot = ((int64_t)px * nx + (int64_t)py * ny + (int64_t)pz * nz) >> 12;
return (dot + d) >= 0; return (dot + d) >= 0;
} }
@@ -65,21 +48,18 @@ static_assert(sizeof(BVHHeader) == 4, "BVHHeader must be 4 bytes");
/// Frustum planes for culling (6 planes) /// Frustum planes for culling (6 planes)
struct Frustum { struct Frustum {
// Each plane: nx, ny, nz (normal), d (distance)
// All in fixed-point 20.12 format
struct Plane { struct Plane {
int32_t nx, ny, nz, d; int32_t nx, ny, nz, d;
}; };
Plane planes[6]; // Near, Far, Left, Right, Top, Bottom Plane planes[6];
/// Test if AABB is visible (not culled by all planes)
bool testAABB(const BVHNode& node) const { bool testAABB(const BVHNode& node) const {
for (int i = 0; i < 6; i++) { for (int i = 0; i < 6; i++) {
if (!node.testPlane(planes[i].nx, planes[i].ny, planes[i].nz, planes[i].d)) { if (!node.testPlane(planes[i].nx, planes[i].ny, planes[i].nz, planes[i].d)) {
return false; // Completely outside this plane return false;
} }
} }
return true; // Potentially visible return true;
} }
}; };
@@ -90,10 +70,6 @@ public:
void initialize(const BVHNode* nodes, uint16_t nodeCount, void initialize(const BVHNode* nodes, uint16_t nodeCount,
const TriangleRef* triangleRefs, uint16_t triangleRefCount); const TriangleRef* triangleRefs, uint16_t triangleRefCount);
/// Initialize from raw splashpack data (alternative)
/// Returns pointer past the BVH data
const uint8_t* initializeFromData(const uint8_t* data, uint16_t nodeCount, uint16_t triangleRefCount);
/// Traverse BVH and collect visible triangle references /// Traverse BVH and collect visible triangle references
/// Uses frustum culling to skip invisible branches /// Uses frustum culling to skip invisible branches
/// Returns number of visible triangle refs /// Returns number of visible triangle refs
@@ -117,6 +93,11 @@ public:
/// Check if BVH is loaded /// Check if BVH is loaded
bool isLoaded() const { return m_nodes != nullptr; } 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: private:
const BVHNode* m_nodes = nullptr; const BVHNode* m_nodes = nullptr;
const TriangleRef* m_triangleRefs = nullptr; const TriangleRef* m_triangleRefs = nullptr;

View File

@@ -6,7 +6,6 @@
#include <psyqo/trigonometry.hh> #include <psyqo/trigonometry.hh>
psxsplash::Camera::Camera() { psxsplash::Camera::Camera() {
// Load identity
m_rotationMatrix = psyqo::SoftMath::generateRotationMatrix33(0, psyqo::SoftMath::Axis::X, m_trig); m_rotationMatrix = psyqo::SoftMath::generateRotationMatrix33(0, psyqo::SoftMath::Axis::X, m_trig);
} }
@@ -23,11 +22,14 @@ void psxsplash::Camera::SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<1
} }
void psxsplash::Camera::SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z) { 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 rotX = psyqo::SoftMath::generateRotationMatrix33(x, psyqo::SoftMath::Axis::X, m_trig);
auto rotY = psyqo::SoftMath::generateRotationMatrix33(y, psyqo::SoftMath::Axis::Y, m_trig); auto rotY = psyqo::SoftMath::generateRotationMatrix33(y, psyqo::SoftMath::Axis::Y, m_trig);
auto rotZ = psyqo::SoftMath::generateRotationMatrix33(z, psyqo::SoftMath::Axis::Z, m_trig); auto rotZ = psyqo::SoftMath::generateRotationMatrix33(z, psyqo::SoftMath::Axis::Z, m_trig);
// XYZ multiplication order (matches C#)
psyqo::SoftMath::multiplyMatrix33(rotY, rotX, &rotY); psyqo::SoftMath::multiplyMatrix33(rotY, rotX, &rotY);
psyqo::SoftMath::multiplyMatrix33(rotY, rotZ, &rotY); psyqo::SoftMath::multiplyMatrix33(rotY, rotZ, &rotY);
@@ -37,39 +39,11 @@ void psxsplash::Camera::SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle
psyqo::Matrix33& psxsplash::Camera::GetRotation() { return m_rotationMatrix; } psyqo::Matrix33& psxsplash::Camera::GetRotation() { return m_rotationMatrix; }
void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const { void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
// =========================================================================
// FRUSTUM CULLING FOR PSX/GTE COORDINATE SYSTEM
// =========================================================================
//
// GTE projection settings (from renderer):
// Screen: 320x240 (half-width=160, half-height=120)
// H = 120 (projection plane distance)
//
// FOV calculation:
// Horizontal half-angle: atan(160/120) ≈ 53° → total ~106° horizontal FOV
// Vertical half-angle: atan(120/120) = 45° → total 90° vertical FOV
//
// For frustum plane normals, we use the ratio of screen edge to H:
// Left/Right planes: normal = forward * screenHalfWidth + right * H
// Top/Bottom planes: normal = forward * screenHalfHeight + up * H
//
// GTE uses right-handed coordinate system:
// +X = Right, +Y = Up, +Z = INTO the screen (forward)
//
// The rotation matrix is the VIEW MATRIX - transforms world→camera space.
// For a view matrix: ROWS are the camera axes in world space.
//
// Frustum plane convention (matching testPlane in bvh.hh):
// Normal points INTO the frustum (toward visible space)
// Point is INSIDE frustum if dot(point, normal) + d >= 0
// =========================================================================
// GTE projection parameters (must match renderer setup) constexpr int32_t SCREEN_HALF_WIDTH = 160;
constexpr int32_t SCREEN_HALF_WIDTH = 160; // 320/2 constexpr int32_t SCREEN_HALF_HEIGHT = 120;
constexpr int32_t SCREEN_HALF_HEIGHT = 120; // 240/2 constexpr int32_t H = 120;
constexpr int32_t H = 120; // Projection distance
// Camera axes in world space (ROWS of view rotation matrix)
int32_t rightX = m_rotationMatrix.vs[0].x.raw(); int32_t rightX = m_rotationMatrix.vs[0].x.raw();
int32_t rightY = m_rotationMatrix.vs[0].y.raw(); int32_t rightY = m_rotationMatrix.vs[0].y.raw();
int32_t rightZ = m_rotationMatrix.vs[0].z.raw(); int32_t rightZ = m_rotationMatrix.vs[0].z.raw();
@@ -86,41 +60,17 @@ void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
int32_t camY = m_position.y.raw(); int32_t camY = m_position.y.raw();
int32_t camZ = m_position.z.raw(); int32_t camZ = m_position.z.raw();
// =========================================================================
// PLANE 0: NEAR PLANE
// Normal points FORWARD (into visible space)
// =========================================================================
frustum.planes[0].nx = fwdX; frustum.planes[0].nx = fwdX;
frustum.planes[0].ny = fwdY; frustum.planes[0].ny = fwdY;
frustum.planes[0].nz = fwdZ; frustum.planes[0].nz = fwdZ;
int64_t fwdDotCam = ((int64_t)fwdX * camX + (int64_t)fwdY * camY + (int64_t)fwdZ * camZ) >> 12; int64_t fwdDotCam = ((int64_t)fwdX * camX + (int64_t)fwdY * camY + (int64_t)fwdZ * camZ) >> 12;
frustum.planes[0].d = -fwdDotCam; frustum.planes[0].d = -fwdDotCam;
// =========================================================================
// PLANE 1: FAR PLANE
// Normal points BACKWARD (toward camera)
// Far distance in fixed 20.12: 4096 = 1 unit, so 4096000 ≈ 1000 units
// =========================================================================
frustum.planes[1].nx = -fwdX; frustum.planes[1].nx = -fwdX;
frustum.planes[1].ny = -fwdY; frustum.planes[1].ny = -fwdY;
frustum.planes[1].nz = -fwdZ; frustum.planes[1].nz = -fwdZ;
frustum.planes[1].d = fwdDotCam + (4096 * 2000); // 2000 units far plane frustum.planes[1].d = fwdDotCam + (4096 * 2000);
// =========================================================================
// SIDE PLANES - Based on actual GTE FOV
//
// The frustum edge in camera space goes through (±screenHalf, 0, H).
// Plane normal (pointing INTO frustum) = right * H + forward * screenHalfWidth
// (for left plane, we add right; for right plane, we subtract right)
//
// Note: axes are in 4.12 fixed point (4096 = 1.0), but H and screen values
// are integers. We scale H to match: H * 4096 / some_factor
// Since we just need the ratio, we can use H and screenHalf directly
// as weights for the axis vectors.
// =========================================================================
// PLANE 2: LEFT PLANE - cull things to the LEFT of view
// Normal = right * H + forward * screenHalfWidth (points into frustum)
frustum.planes[2].nx = ((int64_t)rightX * H + (int64_t)fwdX * SCREEN_HALF_WIDTH) >> 12; 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].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].nz = ((int64_t)rightZ * H + (int64_t)fwdZ * SCREEN_HALF_WIDTH) >> 12;
@@ -128,17 +78,13 @@ void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
(int64_t)frustum.planes[2].ny * camY + (int64_t)frustum.planes[2].ny * camY +
(int64_t)frustum.planes[2].nz * camZ) >> 12); (int64_t)frustum.planes[2].nz * camZ) >> 12);
// PLANE 3: RIGHT PLANE - cull things to the RIGHT of view
// Normal = -right * H + forward * screenHalfWidth (points into frustum)
frustum.planes[3].nx = ((int64_t)(-rightX) * H + (int64_t)fwdX * SCREEN_HALF_WIDTH) >> 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].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].nz = ((int64_t)(-rightZ) * H + (int64_t)fwdZ * SCREEN_HALF_WIDTH) >> 12;
frustum.planes[3].d = -(((int64_t)frustum.planes[3].nx * camX + frustum.planes[3].d = -(((int64_t)frustum.planes[3].nx * camX +
(int64_t)frustum.planes[3].ny * camY + (int64_t)frustum.planes[3].ny * camY +
(int64_t)frustum.planes[3].nz * camZ) >> 12); (int64_t)frustum.planes[3].nz * camZ) >> 12);
// PLANE 4: BOTTOM PLANE - cull things BELOW view
// Normal = up * H + forward * screenHalfHeight (points into frustum)
frustum.planes[4].nx = ((int64_t)upX * H + (int64_t)fwdX * SCREEN_HALF_HEIGHT) >> 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].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].nz = ((int64_t)upZ * H + (int64_t)fwdZ * SCREEN_HALF_HEIGHT) >> 12;
@@ -146,8 +92,6 @@ void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
(int64_t)frustum.planes[4].ny * camY + (int64_t)frustum.planes[4].ny * camY +
(int64_t)frustum.planes[4].nz * camZ) >> 12); (int64_t)frustum.planes[4].nz * camZ) >> 12);
// PLANE 5: TOP PLANE - cull things ABOVE view
// Normal = -up * H + forward * screenHalfHeight (points into frustum)
frustum.planes[5].nx = ((int64_t)(-upX) * H + (int64_t)fwdX * SCREEN_HALF_HEIGHT) >> 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].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].nz = ((int64_t)(-upZ) * H + (int64_t)fwdZ * SCREEN_HALF_HEIGHT) >> 12;

View File

@@ -8,14 +8,13 @@
namespace psxsplash { namespace psxsplash {
// Camera class for managing 3D position and rotation.
class Camera { class Camera {
public: public:
Camera(); Camera();
void MoveX(psyqo::FixedPoint<12> x); void MoveX(psyqo::FixedPoint<12> x);
void MoveY(psyqo::FixedPoint<12> y); void MoveY(psyqo::FixedPoint<12> y);
void MoveZ(psyqo::FixedPoint<12> y); void MoveZ(psyqo::FixedPoint<12> z);
void SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z); void SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z);
psyqo::Vec3& GetPosition() { return m_position; } psyqo::Vec3& GetPosition() { return m_position; }
@@ -23,13 +22,16 @@ class Camera {
void SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z); void SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z);
psyqo::Matrix33& GetRotation(); psyqo::Matrix33& GetRotation();
/// Extract frustum planes for culling
/// Near/Far planes based on typical PS1 draw distances
void ExtractFrustum(Frustum& frustum) const; 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: private:
psyqo::Matrix33 m_rotationMatrix; psyqo::Matrix33 m_rotationMatrix;
psyqo::Trig<> m_trig; psyqo::Trig<> m_trig;
psyqo::Vec3 m_position; psyqo::Vec3 m_position;
int16_t m_angleX = 0, m_angleY = 0, m_angleZ = 0;
}; };
} // namespace psxsplash } // namespace psxsplash

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

View File

@@ -3,17 +3,14 @@
#include <psyqo/fixed-point.hh> #include <psyqo/fixed-point.hh>
// Helper type alias for brevity
using FP = psyqo::FixedPoint<12>; using FP = psyqo::FixedPoint<12>;
namespace psxsplash { namespace psxsplash {
// Static member initialization
psyqo::FixedPoint<12> SpatialGrid::WORLD_MIN = FP(-16); psyqo::FixedPoint<12> SpatialGrid::WORLD_MIN = FP(-16);
psyqo::FixedPoint<12> SpatialGrid::WORLD_MAX = FP(16); psyqo::FixedPoint<12> SpatialGrid::WORLD_MAX = FP(16);
psyqo::FixedPoint<12> SpatialGrid::CELL_SIZE = FP(4); // (32 / 8) = 4 psyqo::FixedPoint<12> SpatialGrid::CELL_SIZE = FP(4);
// AABB expand implementation
void AABB::expand(const psyqo::Vec3& delta) { void AABB::expand(const psyqo::Vec3& delta) {
psyqo::FixedPoint<12> zero; psyqo::FixedPoint<12> zero;
if (delta.x > zero) max.x = max.x + delta.x; if (delta.x > zero) max.x = max.x + delta.x;
@@ -35,7 +32,6 @@ void SpatialGrid::clear() {
} }
void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const { void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const {
// Clamp position to world bounds
auto px = pos.x; auto px = pos.x;
auto py = pos.y; auto py = pos.y;
auto pz = pos.z; auto pz = pos.z;
@@ -47,13 +43,10 @@ void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz)
if (pz < WORLD_MIN) pz = WORLD_MIN; if (pz < WORLD_MIN) pz = WORLD_MIN;
if (pz > WORLD_MAX) pz = WORLD_MAX; if (pz > WORLD_MAX) pz = WORLD_MAX;
// Convert to grid coordinates (0 to GRID_SIZE-1)
// Using integer division after scaling
gx = ((px - WORLD_MIN) / CELL_SIZE).integer(); gx = ((px - WORLD_MIN) / CELL_SIZE).integer();
gy = ((py - WORLD_MIN) / CELL_SIZE).integer(); gy = ((py - WORLD_MIN) / CELL_SIZE).integer();
gz = ((pz - WORLD_MIN) / CELL_SIZE).integer(); gz = ((pz - WORLD_MIN) / CELL_SIZE).integer();
// Clamp to valid range
if (gx < 0) gx = 0; if (gx < 0) gx = 0;
if (gx >= GRID_SIZE) gx = GRID_SIZE - 1; if (gx >= GRID_SIZE) gx = GRID_SIZE - 1;
if (gy < 0) gy = 0; if (gy < 0) gy = 0;
@@ -62,21 +55,13 @@ void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz)
if (gz >= GRID_SIZE) gz = GRID_SIZE - 1; if (gz >= GRID_SIZE) gz = GRID_SIZE - 1;
} }
int SpatialGrid::getCellIndex(const psyqo::Vec3& pos) const {
int gx, gy, gz;
worldToGrid(pos, gx, gy, gz);
return gx + gy * GRID_SIZE + gz * GRID_SIZE * GRID_SIZE;
}
void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) { void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) {
// Get grid range for this AABB
int minGx, minGy, minGz; int minGx, minGy, minGz;
int maxGx, maxGy, maxGz; int maxGx, maxGy, maxGz;
worldToGrid(bounds.min, minGx, minGy, minGz); worldToGrid(bounds.min, minGx, minGy, minGz);
worldToGrid(bounds.max, maxGx, maxGy, maxGz); worldToGrid(bounds.max, maxGx, maxGy, maxGz);
// Insert into all overlapping cells
for (int gz = minGz; gz <= maxGz; gz++) { for (int gz = minGz; gz <= maxGz; gz++) {
for (int gy = minGy; gy <= maxGy; gy++) { for (int gy = minGy; gy <= maxGy; gy++) {
for (int gx = minGx; gx <= maxGx; gx++) { for (int gx = minGx; gx <= maxGx; gx++) {
@@ -86,8 +71,6 @@ void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) {
if (cell.count < MAX_OBJECTS_PER_CELL) { if (cell.count < MAX_OBJECTS_PER_CELL) {
cell.objectIndices[cell.count++] = objectIndex; cell.objectIndices[cell.count++] = objectIndex;
} }
// If cell is full, object won't be in this cell (may miss collisions)
// This is a tradeoff for memory/performance
} }
} }
} }
@@ -96,18 +79,15 @@ void SpatialGrid::insert(uint16_t objectIndex, const AABB& bounds) {
int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const { int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const {
int resultCount = 0; int resultCount = 0;
// Get grid range for query AABB
int minGx, minGy, minGz; int minGx, minGy, minGz;
int maxGx, maxGy, maxGz; int maxGx, maxGy, maxGz;
worldToGrid(bounds.min, minGx, minGy, minGz); worldToGrid(bounds.min, minGx, minGy, minGz);
worldToGrid(bounds.max, maxGx, maxGy, maxGz); worldToGrid(bounds.max, maxGx, maxGy, maxGz);
// Track which objects we've already added (two 32-bit masks for objects 0-63) uint32_t addedMaskLow = 0;
uint32_t addedMaskLow = 0; // Objects 0-31 uint32_t addedMaskHigh = 0;
uint32_t addedMaskHigh = 0; // Objects 32-63
// Query all overlapping cells
for (int gz = minGz; gz <= maxGz; gz++) { for (int gz = minGz; gz <= maxGz; gz++) {
for (int gy = minGy; gy <= maxGy; gy++) { for (int gy = minGy; gy <= maxGy; gy++) {
for (int gx = minGx; gx <= maxGx; gx++) { for (int gx = minGx; gx <= maxGx; gx++) {
@@ -117,7 +97,6 @@ int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults)
for (int i = 0; i < cell.count; i++) { for (int i = 0; i < cell.count; i++) {
uint16_t objIndex = cell.objectIndices[i]; uint16_t objIndex = cell.objectIndices[i];
// Skip if already added (using bitmask for objects 0-63)
if (objIndex < 32) { if (objIndex < 32) {
uint32_t bit = 1U << objIndex; uint32_t bit = 1U << objIndex;
if (addedMaskLow & bit) continue; if (addedMaskLow & bit) continue;
@@ -149,6 +128,7 @@ void CollisionSystem::init() {
void CollisionSystem::reset() { void CollisionSystem::reset() {
m_colliderCount = 0; m_colliderCount = 0;
m_triggerBoxCount = 0;
m_resultCount = 0; m_resultCount = 0;
m_triggerPairCount = 0; m_triggerPairCount = 0;
m_grid.clear(); m_grid.clear();
@@ -156,121 +136,146 @@ void CollisionSystem::reset() {
void CollisionSystem::registerCollider(uint16_t gameObjectIndex, const AABB& localBounds, void CollisionSystem::registerCollider(uint16_t gameObjectIndex, const AABB& localBounds,
CollisionType type, CollisionMask mask) { CollisionType type, CollisionMask mask) {
if (m_colliderCount >= MAX_COLLIDERS) { if (m_colliderCount >= MAX_COLLIDERS) return;
// Out of collider slots
return;
}
CollisionData& data = m_colliders[m_colliderCount++]; CollisionData& data = m_colliders[m_colliderCount++];
data.bounds = localBounds; // Will be transformed in updateCollider data.localBounds = localBounds;
data.bounds = localBounds;
data.type = type; data.type = type;
data.layerMask = mask; data.layerMask = mask;
data.flags = 0;
data.gridCell = 0;
data.gameObjectIndex = gameObjectIndex; 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, void CollisionSystem::updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position,
const psyqo::Matrix33& rotation) { const psyqo::Matrix33& rotation) {
// Find the collider for this object
for (int i = 0; i < m_colliderCount; i++) { for (int i = 0; i < m_colliderCount; i++) {
if (m_colliders[i].gameObjectIndex == gameObjectIndex) { if (m_colliders[i].gameObjectIndex == gameObjectIndex) {
// For now, just translate the AABB (no rotation support for AABBs) m_colliders[i].bounds.min = m_colliders[i].localBounds.min + position;
// TODO: Compute rotated AABB if needed m_colliders[i].bounds.max = m_colliders[i].localBounds.max + position;
// Store original local bounds somewhere if we need to recalculate
// For now, assume bounds are already world-relative
m_colliders[i].bounds.min = m_colliders[i].bounds.min + position;
m_colliders[i].bounds.max = m_colliders[i].bounds.max + position;
break; break;
} }
} }
} }
int CollisionSystem::detectCollisions() { int CollisionSystem::detectCollisions(const AABB& playerAABB, psyqo::Vec3& pushBack, SceneManager& scene) {
m_resultCount = 0; m_resultCount = 0;
const FP zero(0);
// Clear and rebuild spatial grid pushBack = psyqo::Vec3{zero, zero, zero};
// Rebuild spatial grid with active colliders only
m_grid.clear(); m_grid.clear();
for (int i = 0; i < m_colliderCount; i++) { for (int i = 0; i < m_colliderCount; i++) {
m_grid.insert(i, m_colliders[i].bounds); auto* go = scene.getGameObject(m_colliders[i].gameObjectIndex);
} if (go && go->isActive()) {
m_grid.insert(i, m_colliders[i].bounds);
// Check each collider against potential colliders from grid
for (int i = 0; i < m_colliderCount; i++) {
const CollisionData& colliderA = m_colliders[i];
// Skip if no collision type
if (colliderA.type == CollisionType::None) continue;
// Query spatial grid for nearby objects
uint16_t nearby[32];
int nearbyCount = m_grid.queryAABB(colliderA.bounds, nearby, 32);
for (int j = 0; j < nearbyCount; j++) {
int otherIndex = nearby[j];
// Skip self
if (otherIndex == i) continue;
// Skip if already processed (only process pairs once)
if (otherIndex < i) continue;
const CollisionData& colliderB = m_colliders[otherIndex];
// Skip if no collision type
if (colliderB.type == CollisionType::None) continue;
// Check layer masks
if ((colliderA.layerMask & colliderB.layerMask) == 0) continue;
// Narrowphase AABB test
psyqo::Vec3 normal;
psyqo::FixedPoint<12> penetration;
if (testAABB(colliderA.bounds, colliderB.bounds, normal, penetration)) {
// Collision detected
if (m_resultCount < MAX_COLLISION_RESULTS) {
CollisionResult& result = m_results[m_resultCount++];
result.objectA = colliderA.gameObjectIndex;
result.objectB = colliderB.gameObjectIndex;
result.normal = normal;
result.penetration = penetration;
}
// Handle triggers
if (colliderA.type == CollisionType::Trigger) {
updateTriggerState(i, otherIndex, true);
}
if (colliderB.type == CollisionType::Trigger) {
updateTriggerState(otherIndex, i, true);
}
}
} }
} }
// Update trigger pairs that are no longer overlapping // Test player AABB against all colliders for push-back
for (int i = 0; i < m_triggerPairCount; i++) { uint16_t nearby[32];
TriggerPair& pair = m_triggerPairs[i]; int nearbyCount = m_grid.queryAABB(playerAABB, nearby, 32);
pair.framesSinceContact++;
for (int j = 0; j < nearbyCount; j++) {
int idx = nearby[j];
const CollisionData& collider = m_colliders[idx];
if (collider.type == CollisionType::None) continue;
// If no contact for several frames, trigger exit psyqo::Vec3 normal;
if (pair.framesSinceContact > 2 && pair.state != 2) { psyqo::FixedPoint<12> penetration;
pair.state = 2; // Exiting
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; 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, bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const { psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const {
// Check for overlap on all axes
if (a.max.x < b.min.x || a.min.x > b.max.x) return false; 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.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; if (a.max.z < b.min.z || a.min.z > b.max.z) return false;
// Calculate penetration on each axis
auto overlapX1 = a.max.x - b.min.x; auto overlapX1 = a.max.x - b.min.x;
auto overlapX2 = b.max.x - a.min.x; auto overlapX2 = b.max.x - a.min.x;
auto overlapY1 = a.max.y - b.min.y; auto overlapY1 = a.max.y - b.min.y;
@@ -278,17 +283,14 @@ bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
auto overlapZ1 = a.max.z - b.min.z; auto overlapZ1 = a.max.z - b.min.z;
auto overlapZ2 = b.max.z - a.min.z; auto overlapZ2 = b.max.z - a.min.z;
// Find minimum overlap axis
auto minOverlapX = (overlapX1 < overlapX2) ? overlapX1 : overlapX2; auto minOverlapX = (overlapX1 < overlapX2) ? overlapX1 : overlapX2;
auto minOverlapY = (overlapY1 < overlapY2) ? overlapY1 : overlapY2; auto minOverlapY = (overlapY1 < overlapY2) ? overlapY1 : overlapY2;
auto minOverlapZ = (overlapZ1 < overlapZ2) ? overlapZ1 : overlapZ2; auto minOverlapZ = (overlapZ1 < overlapZ2) ? overlapZ1 : overlapZ2;
// Constants for normals
const FP zero(0); const FP zero(0);
const FP one(1); const FP one(1);
const FP negOne(-1); const FP negOne(-1);
// Determine separation axis (axis with least penetration)
if (minOverlapX <= minOverlapY && minOverlapX <= minOverlapZ) { if (minOverlapX <= minOverlapY && minOverlapX <= minOverlapZ) {
penetration = minOverlapX; penetration = minOverlapX;
normal = psyqo::Vec3{(overlapX1 < overlapX2) ? negOne : one, zero, zero}; normal = psyqo::Vec3{(overlapX1 < overlapX2) ? negOne : one, zero, zero};
@@ -303,166 +305,4 @@ bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
return true; return true;
} }
void CollisionSystem::updateTriggerState(uint16_t triggerIndex, uint16_t otherIndex, bool isOverlapping) {
// Look for existing pair
for (int i = 0; i < m_triggerPairCount; i++) {
TriggerPair& pair = m_triggerPairs[i];
if (pair.triggerIndex == triggerIndex && pair.otherIndex == otherIndex) {
if (isOverlapping) {
pair.framesSinceContact = 0;
if (pair.state == 0) {
pair.state = 1; // Now staying
}
}
return;
}
}
// New pair - add it
if (isOverlapping && m_triggerPairCount < MAX_TRIGGERS) {
TriggerPair& pair = m_triggerPairs[m_triggerPairCount++];
pair.triggerIndex = triggerIndex;
pair.otherIndex = otherIndex;
pair.framesSinceContact = 0;
pair.state = 0; // New (enter event)
}
}
bool CollisionSystem::areColliding(uint16_t indexA, uint16_t indexB) const {
for (int i = 0; i < m_resultCount; i++) {
if ((m_results[i].objectA == indexA && m_results[i].objectB == indexB) ||
(m_results[i].objectA == indexB && m_results[i].objectB == indexA)) {
return true;
}
}
return false;
}
bool CollisionSystem::raycast(const psyqo::Vec3& origin, const psyqo::Vec3& direction,
psyqo::FixedPoint<12> maxDistance,
psyqo::Vec3& hitPoint, psyqo::Vec3& hitNormal,
uint16_t& hitObjectIndex) const {
// Simple brute-force raycast against all colliders
// TODO: Use spatial grid for optimization
auto closestT = maxDistance;
bool hit = false;
// Fixed-point constants
const FP zero(0);
const FP one(1);
const FP negOne(-1);
const FP largeVal(1000);
const FP negLargeVal(-1000);
FP epsilon;
epsilon.value = 4; // ~0.001 in 20.12 fixed point
for (int i = 0; i < m_colliderCount; i++) {
const CollisionData& collider = m_colliders[i];
if (collider.type == CollisionType::None) continue;
// Ray-AABB intersection test (slab method)
const AABB& box = collider.bounds;
auto tMin = negLargeVal;
auto tMax = largeVal;
// X slab
if (direction.x != zero) {
auto invD = one / direction.x;
auto t1 = (box.min.x - origin.x) * invD;
auto t2 = (box.max.x - origin.x) * invD;
if (t1 > t2) { auto tmp = t1; t1 = t2; t2 = tmp; }
if (t1 > tMin) tMin = t1;
if (t2 < tMax) tMax = t2;
} else if (origin.x < box.min.x || origin.x > box.max.x) {
continue;
}
// Y slab
if (direction.y != zero) {
auto invD = one / direction.y;
auto t1 = (box.min.y - origin.y) * invD;
auto t2 = (box.max.y - origin.y) * invD;
if (t1 > t2) { auto tmp = t1; t1 = t2; t2 = tmp; }
if (t1 > tMin) tMin = t1;
if (t2 < tMax) tMax = t2;
} else if (origin.y < box.min.y || origin.y > box.max.y) {
continue;
}
// Z slab
if (direction.z != zero) {
auto invD = one / direction.z;
auto t1 = (box.min.z - origin.z) * invD;
auto t2 = (box.max.z - origin.z) * invD;
if (t1 > t2) { auto tmp = t1; t1 = t2; t2 = tmp; }
if (t1 > tMin) tMin = t1;
if (t2 < tMax) tMax = t2;
} else if (origin.z < box.min.z || origin.z > box.max.z) {
continue;
}
if (tMin > tMax || tMax < zero) continue;
auto t = (tMin >= zero) ? tMin : tMax;
if (t < closestT && t >= zero) {
closestT = t;
hitObjectIndex = collider.gameObjectIndex;
hit = true;
// Calculate hit point
hitPoint = psyqo::Vec3{
origin.x + direction.x * t,
origin.y + direction.y * t,
origin.z + direction.z * t
};
// Calculate normal (which face was hit)
if ((hitPoint.x - box.min.x).abs() < epsilon) hitNormal = psyqo::Vec3{negOne, zero, zero};
else if ((hitPoint.x - box.max.x).abs() < epsilon) hitNormal = psyqo::Vec3{one, zero, zero};
else if ((hitPoint.y - box.min.y).abs() < epsilon) hitNormal = psyqo::Vec3{zero, negOne, zero};
else if ((hitPoint.y - box.max.y).abs() < epsilon) hitNormal = psyqo::Vec3{zero, one, zero};
else if ((hitPoint.z - box.min.z).abs() < epsilon) hitNormal = psyqo::Vec3{zero, zero, negOne};
else hitNormal = psyqo::Vec3{zero, zero, one};
}
}
return hit;
}
void CollisionSystem::processTriggerEvents(SceneManager& scene) {
// Process trigger pairs and fire Lua events
int writeIndex = 0;
for (int i = 0; i < m_triggerPairCount; i++) {
TriggerPair& pair = m_triggerPairs[i];
// Get game object indices
uint16_t triggerObjIdx = m_colliders[pair.triggerIndex].gameObjectIndex;
uint16_t otherObjIdx = m_colliders[pair.otherIndex].gameObjectIndex;
switch (pair.state) {
case 0: // Enter
scene.fireTriggerEnter(triggerObjIdx, otherObjIdx);
pair.state = 1; // Move to staying
m_triggerPairs[writeIndex++] = pair;
break;
case 1: // Staying
scene.fireTriggerStay(triggerObjIdx, otherObjIdx);
m_triggerPairs[writeIndex++] = pair;
break;
case 2: // Exit
scene.fireTriggerExit(triggerObjIdx, otherObjIdx);
// Don't copy - remove from list
break;
}
}
m_triggerPairCount = writeIndex;
}
} // namespace psxsplash } // namespace psxsplash

View File

@@ -1,17 +1,5 @@
#pragma once #pragma once
/**
* collision.hh - PS1 Collision System
*
* Provides spatial hashing broadphase and AABB narrowphase collision detection.
* Designed for PS1's limited CPU - uses fixed-point math and spatial partitioning.
*
* Architecture:
* - Broadphase: Spatial grid (cells of fixed size)
* - Narrowphase: AABB intersection tests
* - Trigger system: Enter/Stay/Exit events
*/
#include <psyqo/fixed-point.hh> #include <psyqo/fixed-point.hh>
#include <psyqo/vector.hh> #include <psyqo/vector.hh>
#include <EASTL/vector.h> #include <EASTL/vector.h>
@@ -20,220 +8,134 @@
namespace psxsplash { namespace psxsplash {
// Forward declarations
class SceneManager; class SceneManager;
/**
* Collision type flags - matches Unity PSXCollisionType enum
*/
enum class CollisionType : uint8_t { enum class CollisionType : uint8_t {
None = 0, None = 0,
Solid = 1, // Blocks movement Solid = 1,
Trigger = 2, // Fires events, doesn't block
Platform = 3 // Solid from above only
}; };
/**
* Collision layer mask - 8 layers available
* Objects only collide with matching layers
*/
using CollisionMask = uint8_t; using CollisionMask = uint8_t;
/**
* Axis-Aligned Bounding Box in fixed-point
* Used for broadphase and narrowphase collision
*/
struct AABB { struct AABB {
psyqo::Vec3 min; psyqo::Vec3 min;
psyqo::Vec3 max; psyqo::Vec3 max;
// Check if this AABB intersects another
bool intersects(const AABB& other) const { bool intersects(const AABB& other) const {
return (min.x <= other.max.x && max.x >= other.min.x) && return (min.x <= other.max.x && max.x >= other.min.x) &&
(min.y <= other.max.y && max.y >= other.min.y) && (min.y <= other.max.y && max.y >= other.min.y) &&
(min.z <= other.max.z && max.z >= other.min.z); (min.z <= other.max.z && max.z >= other.min.z);
} }
// Check if a point is inside this AABB
bool contains(const psyqo::Vec3& point) const { bool contains(const psyqo::Vec3& point) const {
return (point.x >= min.x && point.x <= max.x) && return (point.x >= min.x && point.x <= max.x) &&
(point.y >= min.y && point.y <= max.y) && (point.y >= min.y && point.y <= max.y) &&
(point.z >= min.z && point.z <= max.z); (point.z >= min.z && point.z <= max.z);
} }
// Get center of AABB
psyqo::Vec3 center() const {
return psyqo::Vec3{
(min.x + max.x) / 2,
(min.y + max.y) / 2,
(min.z + max.z) / 2
};
}
// Get half-extents
psyqo::Vec3 halfExtents() const {
return psyqo::Vec3{
(max.x - min.x) / 2,
(max.y - min.y) / 2,
(max.z - min.z) / 2
};
}
// Expand AABB by a vector (for swept tests)
void expand(const psyqo::Vec3& delta); void expand(const psyqo::Vec3& delta);
}; };
static_assert(sizeof(AABB) == 24, "AABB must be 24 bytes (2x Vec3)"); static_assert(sizeof(AABB) == 24);
/**
* Collision data for a single object
* Stored separately from GameObject for cache efficiency
*/
struct CollisionData { struct CollisionData {
AABB bounds; // World-space AABB (24 bytes) AABB localBounds;
CollisionType type; // Collision behavior (1 byte) AABB bounds;
CollisionMask layerMask; // Which layers this collides with (1 byte) CollisionType type;
uint8_t flags; // Additional flags (1 byte) CollisionMask layerMask;
uint8_t gridCell; // Current spatial grid cell (1 byte) uint16_t gameObjectIndex;
uint16_t gameObjectIndex; // Index into GameObject array (2 bytes)
uint16_t padding; // Alignment padding (2 bytes)
}; };
static_assert(sizeof(CollisionData) == 32, "CollisionData must be 32 bytes");
/**
* Collision result - returned when collision is detected
*/
struct CollisionResult { struct CollisionResult {
uint16_t objectA; // First object index uint16_t objectA;
uint16_t objectB; // Second object index uint16_t objectB;
psyqo::Vec3 normal; // Collision normal (from A to B) psyqo::Vec3 normal;
psyqo::FixedPoint<12> penetration; // Penetration depth psyqo::FixedPoint<12> penetration;
}; };
/** struct TriggerBoxData {
* Trigger state for tracking enter/stay/exit AABB bounds;
*/ int16_t luaFileIndex;
struct TriggerPair {
uint16_t triggerIndex; // Index of trigger object
uint16_t otherIndex; // Index of other object
uint8_t framesSinceContact; // Counter for exit detection
uint8_t state; // 0=new, 1=staying, 2=exiting
uint16_t padding; uint16_t padding;
}; };
/** struct TriggerPair {
* Spatial Grid for broadphase collision uint16_t triggerIndex;
* Divides world into fixed-size cells for fast overlap queries uint16_t padding;
*/ uint8_t framesSinceContact;
uint8_t state; // 0=new(enter), 1=active, 2=exiting
uint16_t padding2;
};
class SpatialGrid { class SpatialGrid {
public: public:
// Grid configuration static constexpr int GRID_SIZE = 8;
static constexpr int GRID_SIZE = 8; // 8x8x8 grid
static constexpr int CELL_COUNT = GRID_SIZE * GRID_SIZE * GRID_SIZE; static constexpr int CELL_COUNT = GRID_SIZE * GRID_SIZE * GRID_SIZE;
static constexpr int MAX_OBJECTS_PER_CELL = 16; static constexpr int MAX_OBJECTS_PER_CELL = 16;
// World bounds (fixed for simplicity) - values set in collision.cpp
static psyqo::FixedPoint<12> WORLD_MIN; static psyqo::FixedPoint<12> WORLD_MIN;
static psyqo::FixedPoint<12> WORLD_MAX; static psyqo::FixedPoint<12> WORLD_MAX;
static psyqo::FixedPoint<12> CELL_SIZE; static psyqo::FixedPoint<12> CELL_SIZE;
struct Cell { struct Cell {
uint16_t objectIndices[MAX_OBJECTS_PER_CELL]; uint16_t objectIndices[MAX_OBJECTS_PER_CELL];
uint8_t count; uint8_t count;
uint8_t padding[3]; uint8_t padding[3];
}; };
// Clear all cells
void clear(); void clear();
// Insert an object into the grid
void insert(uint16_t objectIndex, const AABB& bounds); void insert(uint16_t objectIndex, const AABB& bounds);
// Get all potential colliders for an AABB
// Returns number of results written to output
int queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const; int queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const;
// Get cell index for a position
int getCellIndex(const psyqo::Vec3& pos) const;
private: private:
Cell m_cells[CELL_COUNT]; Cell m_cells[CELL_COUNT];
// Convert world position to grid coordinates
void worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const; void worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const;
}; };
/**
* Main Collision System
* Manages all collision detection and trigger events
*/
class CollisionSystem { class CollisionSystem {
public: public:
static constexpr int MAX_COLLIDERS = 64; static constexpr int MAX_COLLIDERS = 64;
static constexpr int MAX_TRIGGERS = 32; static constexpr int MAX_TRIGGER_BOXES = 32;
static constexpr int MAX_TRIGGER_PAIRS = 32;
static constexpr int MAX_COLLISION_RESULTS = 32; static constexpr int MAX_COLLISION_RESULTS = 32;
CollisionSystem() = default; CollisionSystem() = default;
// Initialize the system
void init(); void init();
// Reset for new scene
void reset(); void reset();
// Register a collider (called during scene load) void registerCollider(uint16_t gameObjectIndex, const AABB& localBounds,
void registerCollider(uint16_t gameObjectIndex, const AABB& localBounds,
CollisionType type, CollisionMask mask); CollisionType type, CollisionMask mask);
// Update collision data for an object (call when object moves)
void updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position, void updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position,
const psyqo::Matrix33& rotation); const psyqo::Matrix33& rotation);
// Run collision detection for one frame void registerTriggerBox(const AABB& bounds, int16_t luaFileIndex);
// Returns number of collisions detected
int detectCollisions(); int detectCollisions(const AABB& playerAABB, psyqo::Vec3& pushBack, class SceneManager& scene);
// Get collision results (valid until next detectCollisions call) void detectTriggers(const AABB& playerAABB, class SceneManager& scene);
const CollisionResult* getResults() const { return m_results; } const CollisionResult* getResults() const { return m_results; }
int getResultCount() const { return m_resultCount; } int getResultCount() const { return m_resultCount; }
// Check if two specific objects are colliding
bool areColliding(uint16_t indexA, uint16_t indexB) const;
// Raycast against all colliders
// Returns true if hit, fills hitPoint and hitNormal
bool raycast(const psyqo::Vec3& origin, const psyqo::Vec3& direction,
psyqo::FixedPoint<12> maxDistance,
psyqo::Vec3& hitPoint, psyqo::Vec3& hitNormal,
uint16_t& hitObjectIndex) const;
// Get trigger events for current frame (call from SceneManager)
void processTriggerEvents(class SceneManager& scene);
// Debug: Get collider count
int getColliderCount() const { return m_colliderCount; } int getColliderCount() const { return m_colliderCount; }
private: private:
// Collision data for all registered colliders
CollisionData m_colliders[MAX_COLLIDERS]; CollisionData m_colliders[MAX_COLLIDERS];
int m_colliderCount = 0; int m_colliderCount = 0;
// Spatial partitioning grid TriggerBoxData m_triggerBoxes[MAX_TRIGGER_BOXES];
int m_triggerBoxCount = 0;
SpatialGrid m_grid; SpatialGrid m_grid;
// Collision results for current frame
CollisionResult m_results[MAX_COLLISION_RESULTS]; CollisionResult m_results[MAX_COLLISION_RESULTS];
int m_resultCount = 0; int m_resultCount = 0;
// Trigger tracking TriggerPair m_triggerPairs[MAX_TRIGGER_PAIRS];
TriggerPair m_triggerPairs[MAX_TRIGGERS];
int m_triggerPairCount = 0; int m_triggerPairCount = 0;
// Narrowphase AABB test bool testAABB(const AABB& a, const AABB& b,
bool testAABB(const AABB& a, const AABB& b,
psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const; psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const;
// Update trigger state machine
void updateTriggerState(uint16_t triggerIndex, uint16_t otherIndex, bool isOverlapping);
}; };
} // namespace psxsplash } // namespace psxsplash

View File

@@ -1,7 +1,92 @@
#include "controls.hh" #include "controls.hh"
#include <psyqo/hardware/cpu.hh>
#include <psyqo/hardware/sio.hh>
#include <psyqo/vector.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(); } void psxsplash::Controls::Init() { m_input.initialize(); }
bool psxsplash::Controls::isDigitalPad() const { bool psxsplash::Controls::isDigitalPad() const {

View File

@@ -12,6 +12,10 @@ using namespace psyqo::trig_literals;
class Controls { class Controls {
public: public:
/// Force DualShock into analog mode
/// Must be called BEFORE Init() since Init() hands SIO control to AdvancedPad.
void forceAnalogMode();
void Init(); void Init();
void HandleControls(psyqo::Vec3 &playerPosition, psyqo::Angle &playerRotationX, psyqo::Angle &playerRotationY, void HandleControls(psyqo::Vec3 &playerPosition, psyqo::Angle &playerRotationX, psyqo::Angle &playerRotationY,
psyqo::Angle &playerRotationZ, bool freecam, int deltaFrames); psyqo::Angle &playerRotationZ, bool freecam, int deltaFrames);
@@ -83,4 +87,4 @@ class Controls {
void getDpadAxes(int16_t &outX, int16_t &outY) const; void getDpadAxes(int16_t &outX, int16_t &outY) const;
}; };
}; // namespace psxsplash } // 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
};

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

@@ -4,4 +4,4 @@
namespace psxsplash { namespace psxsplash {
void MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result); void MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result);
}; } // namespace psxsplash

View File

@@ -7,48 +7,65 @@ namespace psxsplash {
/** /**
* Interactable component - enables player interaction with objects. * Interactable component - enables player interaction with objects.
* *
* When the player is within interaction radius and presses the interact button, * When the player is within interaction radius and presses the interact button,
* the onInteract Lua event fires on the associated GameObject. * the onInteract Lua event fires on the associated GameObject.
*/ */
struct Interactable { struct Interactable {
// Interaction radius squared (fixed-point 12-bit, pre-squared for fast distance checks) // Interaction radius squared (fixed-point 12-bit, pre-squared for fast distance checks)
psyqo::FixedPoint<12> radiusSquared; psyqo::FixedPoint<12> radiusSquared;
// Interaction point offset from object center // Button index that triggers interaction (0-15, matches psyqo::AdvancedPad::Button)
psyqo::FixedPoint<12> offsetX;
psyqo::FixedPoint<12> offsetY;
psyqo::FixedPoint<12> offsetZ;
// Button index that triggers interaction (0-15)
uint8_t interactButton; uint8_t interactButton;
// Configuration flags // Configuration flags
uint8_t flags; // bit 0: isRepeatable, bit 1: showPrompt, bit 2: requireLineOfSight uint8_t flags; // bit 0: isRepeatable, bit 1: showPrompt, bit 2: requireLineOfSight, bit 3: disabled
// Cooldown between interactions (in frames) // Cooldown between interactions (in frames)
uint16_t cooldownFrames; uint16_t cooldownFrames;
// Runtime state // Runtime state
uint16_t currentCooldown; // Frames remaining until can interact again uint16_t currentCooldown; // Frames remaining until can interact again
uint16_t gameObjectIndex; // Index of associated GameObject uint16_t gameObjectIndex; // Index of associated GameObject
// Prompt canvas name (null-terminated, max 15 chars + null)
char promptCanvasName[16];
// Flag accessors // Flag accessors
bool isRepeatable() const { return flags & 0x01; } bool isRepeatable() const { return flags & 0x01; }
bool showPrompt() const { return flags & 0x02; } bool showPrompt() const { return flags & 0x02; }
bool requireLineOfSight() const { return flags & 0x04; } 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 // Check if ready to interact
bool canInteract() const { return currentCooldown == 0; } 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 // Called when interaction happens
void triggerCooldown() { currentCooldown = cooldownFrames; } void triggerCooldown() {
if (isRepeatable()) {
currentCooldown = cooldownFrames;
} else {
// Non-repeatable: set to max to permanently disable
currentCooldown = 0xFFFF;
}
}
// Called each frame to decrement cooldown // Called each frame to decrement cooldown
void update() { void update() {
if (currentCooldown > 0) currentCooldown--; if (currentCooldown > 0 && currentCooldown != 0xFFFF) currentCooldown--;
} }
}; };
static_assert(sizeof(Interactable) == 24, "Interactable is not 24 bytes"); static_assert(sizeof(Interactable) == 28, "Interactable must be 28 bytes");
} // namespace psxsplash } // 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

View File

@@ -2,71 +2,48 @@
#include <psyqo-lua/lua.hh> #include <psyqo-lua/lua.hh>
#include <psyqo/alloc.h>
#include <psyqo/soft-math.hh> #include <psyqo/soft-math.hh>
#include <psyqo/trigonometry.hh> #include <psyqo/trigonometry.hh>
#include <psyqo/xprintf.h> #include <psyqo/xprintf.h>
#include "gameobject.hh" #include "gameobject.hh"
constexpr const char GAMEOBJECT_SCRIPT[] = R"( // OOM-guarded allocator for Lua. The linker redirects luaI_realloc
return function(metatable) // here instead of straight to psyqo_realloc, so we can log before
local get_position = metatable.get_position // returning NULL.
local set_position = metatable.set_position extern "C" void *lua_oom_realloc(void *ptr, size_t size) {
local get_active = metatable.get_active void *result = psyqo_realloc(ptr, size);
local set_active = metatable.set_active if (!result && size > 0) {
local get_rotationY = metatable.get_rotationY printf("Lua OOM: alloc %u bytes failed\n", (unsigned)size);
local set_rotationY = metatable.set_rotationY }
return result;
}
metatable.get_position = nil // Pre-compiled PS1 Lua bytecode for the GameObject metatable script.
metatable.set_position = nil // Compiled with luac_psx to avoid needing the Lua parser at runtime.
metatable.get_active = nil #include "gameobject_bytecode.h"
metatable.set_active = 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 == "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 == "rotationY" then
set_rotationY(self.__cpp_ptr, value)
return
end
rawset(self, key, value)
end
end
)";
// Lua helpers // Lua helpers
static constexpr lua_Number kFixedScale = 4096; 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) { static int gameobjectGetPosition(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1); auto go = L.toUserdata<psxsplash::GameObject>(1);
L.newTable(); L.newTable();
L.pushNumber(static_cast<lua_Number>(go->position.x.raw()) / kFixedScale); L.push(go->position.x);
L.setField(2, "x"); L.setField(2, "x");
L.pushNumber(static_cast<lua_Number>(go->position.y.raw()) / kFixedScale); L.push(go->position.y);
L.setField(2, "y"); L.setField(2, "y");
L.pushNumber(static_cast<lua_Number>(go->position.z.raw()) / kFixedScale); L.push(go->position.z);
L.setField(2, "z"); L.setField(2, "z");
return 1; return 1;
@@ -78,40 +55,35 @@ static int gameobjectSetPosition(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1); auto go = L.toUserdata<psxsplash::GameObject>(1);
L.getField(2, "x"); L.getField(2, "x");
go->position.x = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); go->position.x = readFP(L, 3);
L.pop(); L.pop();
L.getField(2, "y"); L.getField(2, "y");
go->position.y = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); go->position.y = readFP(L, 3);
L.pop(); L.pop();
L.getField(2, "z");
go->position.z = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW); L.getField(2, "z");
go->position.z = readFP(L, 3);
L.pop(); L.pop();
return 0; return 0;
} }
static int gamobjectGetActive(psyqo::Lua L) { static int gameobjectGetActive(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1); auto go = L.toUserdata<psxsplash::GameObject>(1);
L.push(go->isActive()); L.push(go->isActive());
return 1; return 1;
} }
static int gamobjectSetActive(psyqo::Lua L) { static int gameobjectSetActive(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1); auto go = L.toUserdata<psxsplash::GameObject>(1);
bool active = L.toBoolean(2); bool active = L.toBoolean(2);
go->setActive(active); go->setActive(active);
return 0; return 0;
} }
// Angle constants: psyqo::Angle is FixedPoint<10>, so 1.0_pi = raw 1024
static constexpr lua_Number kAngleScale = 1024;
static psyqo::Trig<> s_trig; static psyqo::Trig<> s_trig;
// Fast integer atan2 approximation → psyqo::Angle (pi-fraction units)
// Uses linear approximation in first octant then folds to full circle.
// Max error ~4° (acceptable for PS1 game objects).
static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) { static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) {
psyqo::Angle result; psyqo::Angle result;
if (cosVal == 0 && sinVal == 0) { result.value = 0; return result; } if (cosVal == 0 && sinVal == 0) { result.value = 0; return result; }
@@ -121,39 +93,95 @@ static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) {
int32_t minV = abs_s < abs_c ? abs_s : abs_c; int32_t minV = abs_s < abs_c ? abs_s : abs_c;
int32_t maxV = abs_s > abs_c ? abs_s : abs_c; int32_t maxV = abs_s > abs_c ? abs_s : abs_c;
// Compute angle in first octant [0, π/4 = 256 Angle units]
// angle = (minV/maxV) * 256, using only 32-bit math.
// Max minV for normalized sin/cos ≈ 4096, so minV * 256 ≈ 1M — fits int32.
int32_t angle = (minV * 256) / maxV; int32_t angle = (minV * 256) / maxV;
// Past 45°: use complement if (abs_s > abs_c) angle = 512 - angle;
if (abs_s > abs_c) angle = 512 - angle; // π/2 - angle if (cosVal < 0) angle = 1024 - angle;
// Quadrant 2/3: cos < 0
if (cosVal < 0) angle = 1024 - angle; // π - angle
// Quadrant 3/4: sin < 0
if (sinVal < 0) angle = -angle; if (sinVal < 0) angle = -angle;
result.value = angle; result.value = angle;
return result; 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) { static int gameobjectGetRotationY(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1); auto go = L.toUserdata<psxsplash::GameObject>(1);
// Y rotation matrix: vs[0].x = cos(θ), vs[0].z = sin(θ)
int32_t sinRaw = go->rotation.vs[0].z.raw(); int32_t sinRaw = go->rotation.vs[0].z.raw();
int32_t cosRaw = go->rotation.vs[0].x.raw(); int32_t cosRaw = go->rotation.vs[0].x.raw();
psyqo::Angle angle = fastAtan2(sinRaw, cosRaw); psyqo::Angle angle = fastAtan2(sinRaw, cosRaw);
// Return in pi-units: 0.5 = π/2 = 90° // Angle is FixedPoint<10> (pi-units). Convert to FixedPoint<12> for Lua.
L.pushNumber(static_cast<lua_Number>(angle.value) / kAngleScale); psyqo::FixedPoint<12> fp12;
fp12.value = angle.value << 2;
L.push(fp12);
return 1; return 1;
} }
static int gameobjectSetRotationY(psyqo::Lua L) { static int gameobjectSetRotationY(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1); auto go = L.toUserdata<psxsplash::GameObject>(1);
lua_Number piUnits = L.toNumber(2); // Accept FixedPoint<12> from Lua, convert to Angle (FixedPoint<10>)
psyqo::FixedPoint<12> fp12 = readFP(L, 2);
psyqo::Angle angle; psyqo::Angle angle;
angle.value = static_cast<int32_t>(piUnits * kAngleScale); angle.value = fp12.value >> 2;
go->rotation = psyqo::SoftMath::generateRotationMatrix33(angle, psyqo::SoftMath::Axis::Y, s_trig); go->rotation = psyqo::SoftMath::generateRotationMatrix33(angle, psyqo::SoftMath::Axis::Y, s_trig);
return 0; return 0;
} }
@@ -161,7 +189,7 @@ static int gameobjectSetRotationY(psyqo::Lua L) {
void psxsplash::Lua::Init() { void psxsplash::Lua::Init() {
auto L = m_state; auto L = m_state;
// Load and run the game objects script // Load and run the game objects script
if (L.loadBuffer(GAMEOBJECT_SCRIPT, "buffer:gameObjects") == 0) { if (L.loadBuffer(reinterpret_cast<const char*>(GAMEOBJECT_BYTECODE), sizeof(GAMEOBJECT_BYTECODE), "bytecode:gameObjects") == 0) {
if (L.pcall(0, 1) == 0) { if (L.pcall(0, 1) == 0) {
// This will be our metatable // This will be our metatable
L.newTable(); L.newTable();
@@ -172,12 +200,18 @@ void psxsplash::Lua::Init() {
L.push(gameobjectSetPosition); L.push(gameobjectSetPosition);
L.setField(-2, "set_position"); L.setField(-2, "set_position");
L.push(gamobjectGetActive); L.push(gameobjectGetActive);
L.setField(-2, "get_active"); L.setField(-2, "get_active");
L.push(gamobjectSetActive); L.push(gameobjectSetActive);
L.setField(-2, "set_active"); 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.push(gameobjectGetRotationY);
L.setField(-2, "get_rotationY"); L.setField(-2, "get_rotationY");
@@ -209,11 +243,53 @@ void psxsplash::Lua::Init() {
L.newTable(); L.newTable();
m_luascriptsReference = L.ref(); 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() { void psxsplash::Lua::Shutdown() {
// Close the Lua VM if it's still open.
// Safe to call multiple times or on an already-closed VM.
if (m_state.getState()) { if (m_state.getState()) {
m_state.close(); m_state.close();
} }
@@ -223,13 +299,18 @@ void psxsplash::Lua::Shutdown() {
} }
void psxsplash::Lua::Reset() { void psxsplash::Lua::Reset() {
// Nuclear reset: destroy the entire Lua VM and create a fresh one.
Shutdown(); Shutdown();
m_state = psyqo::Lua(); // fresh state (luaL_newstate + openlibs) m_state = psyqo::Lua();
Init(); Init();
} }
void psxsplash::Lua::LoadLuaFile(const char* code, size_t len, int index) { 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; auto L = m_state;
char filename[32]; char filename[32];
snprintf(filename, sizeof(filename), "lua_asset:%d", index); snprintf(filename, sizeof(filename), "lua_asset:%d", index);
@@ -243,7 +324,7 @@ void psxsplash::Lua::LoadLuaFile(const char* code, size_t len, int index) {
// (1) script func (2) scripts table // (1) script func (2) scripts table
L.newTable(); L.newTable();
// (1) script func (2) scripts table (3) env {} // (1) script func (2) scripts table (3) env {}
// Give the environment a metatable that falls back to _G // Give the environment a metatable that falls back to _G
// so scripts can see Entity, Debug, Input, etc. // so scripts can see Entity, Debug, Input, etc.
L.newTable(); L.newTable();
@@ -254,7 +335,7 @@ void psxsplash::Lua::LoadLuaFile(const char* code, size_t len, int index) {
// (1) script func (2) scripts table (3) env {} (4) mt { __index = _G } // (1) script func (2) scripts table (3) env {} (4) mt { __index = _G }
L.setMetatable(-2); L.setMetatable(-2);
// (1) script func (2) scripts table (3) env { mt } // (1) script func (2) scripts table (3) env { mt }
L.pushNumber(index); L.pushNumber(index);
// (1) script func (2) scripts table (3) env (4) index // (1) script func (2) scripts table (3) env (4) index
L.copy(-2); L.copy(-2);
@@ -298,8 +379,6 @@ void psxsplash::Lua::RegisterSceneScripts(int index) {
// empty stack // empty stack
} }
// We're going to store the Lua table for the object at the address of the object,
// and the table for its methods at the address of the object + 1 byte.
void psxsplash::Lua::RegisterGameObject(GameObject* go) { void psxsplash::Lua::RegisterGameObject(GameObject* go) {
uint8_t* ptr = reinterpret_cast<uint8_t*>(go); uint8_t* ptr = reinterpret_cast<uint8_t*>(go);
auto L = m_state; auto L = m_state;
@@ -333,55 +412,87 @@ void psxsplash::Lua::RegisterGameObject(GameObject* go) {
// Initialize event mask for this object // Initialize event mask for this object
uint32_t eventMask = EVENT_NONE; uint32_t eventMask = EVENT_NONE;
if (go->luaFileIndex != -1) { if (go->luaFileIndex != -1 && go->luaFileIndex < m_bytecodeRefCount) {
L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference); auto& ref = m_bytecodeRefs[go->luaFileIndex];
// (1) {} (2) script environments table char filename[32];
L.rawGetI(-1, go->luaFileIndex); snprintf(filename, sizeof(filename), "lua_asset:%d", go->luaFileIndex);
// (1) {} (2) script environments table (3) script environment table for this object
if (L.loadBuffer(ref.code, ref.len, filename) == LUA_OK) {
// Guard: if the script file failed to load (e.g. compilation error), // (1) method_table (2) chunk_func
// the environment will be nil — skip event resolution.
if (!L.isTable(-1)) { // Create a per-object environment with __index = _G
L.pop(2); // 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 { } else {
printf("Lua error: %s\n", L.toString(-1));
// Resolve each event and build the bitmask L.pop(); // pop error msg
// Only events that exist in the script get their bit set
if (onCreateMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_CREATE;
if (onCollisionMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_COLLISION;
if (onInteractMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_INTERACT;
if (onTriggerEnterMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_TRIGGER_ENTER;
if (onTriggerStayMethodWrapper.resolveGlobal(L)) eventMask |= EVENT_ON_TRIGGER_STAY;
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);
// (1) {}
} }
} }
// Store the event mask directly in the GameObject // Store the event mask directly in the GameObject
go->eventMask = eventMask; go->eventMask = eventMask;
L.pop(); L.pop();
// empty stack // 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) {
// Fire onCreate event if this object handles it for (size_t i = 0; i < count; i++) {
if (eventMask & EVENT_ON_CREATE) { if (objects[i] && (objects[i]->eventMask & EVENT_ON_CREATE)) {
onCreateMethodWrapper.callMethod(*this, go); onCreateMethodWrapper.callMethod(*this, objects[i]);
}
} }
} }
void psxsplash::Lua::OnCollision(GameObject* self, GameObject* other) { void psxsplash::Lua::OnCollideWithPlayer(GameObject* self) {
if (!hasEvent(self, EVENT_ON_COLLISION)) return; if (!hasEvent(self, EVENT_ON_COLLISION)) return;
onCollisionMethodWrapper.callMethod(*this, self, other); onCollideWithPlayerMethodWrapper.callMethod(*this, self);
} }
void psxsplash::Lua::OnInteract(GameObject* self) { void psxsplash::Lua::OnInteract(GameObject* self) {
@@ -394,20 +505,44 @@ void psxsplash::Lua::OnTriggerEnter(GameObject* trigger, GameObject* other) {
onTriggerEnterMethodWrapper.callMethod(*this, trigger, other); onTriggerEnterMethodWrapper.callMethod(*this, trigger, other);
} }
void psxsplash::Lua::OnTriggerStay(GameObject* trigger, GameObject* other) {
if (!hasEvent(trigger, EVENT_ON_TRIGGER_STAY)) return;
onTriggerStayMethodWrapper.callMethod(*this, trigger, other);
}
void psxsplash::Lua::OnTriggerExit(GameObject* trigger, GameObject* other) { void psxsplash::Lua::OnTriggerExit(GameObject* trigger, GameObject* other) {
if (!hasEvent(trigger, EVENT_ON_TRIGGER_EXIT)) return; if (!hasEvent(trigger, EVENT_ON_TRIGGER_EXIT)) return;
onTriggerExitMethodWrapper.callMethod(*this, trigger, other); 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) { void psxsplash::Lua::OnDestroy(GameObject* go) {
if (!hasEvent(go, EVENT_ON_DESTROY)) return; if (!hasEvent(go, EVENT_ON_DESTROY)) return;
onDestroyMethodWrapper.callMethod(*this, go); onDestroyMethodWrapper.callMethod(*this, go);
// Clear the event mask when object is destroyed
go->eventMask = EVENT_NONE; go->eventMask = EVENT_NONE;
} }
@@ -436,13 +571,52 @@ void psxsplash::Lua::OnUpdate(GameObject* go, int deltaFrames) {
onUpdateMethodWrapper.callMethod(*this, go, deltaFrames); 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) { void psxsplash::Lua::PushGameObject(GameObject* go) {
auto L = m_state; auto L = m_state;
L.push(go); L.push(go);
L.rawGet(LUA_REGISTRYINDEX); L.rawGet(LUA_REGISTRYINDEX);
if (!L.isTable(-1)) { if (!L.isTable(-1)) {
printf("Warning: GameObject not found in Lua registry\n");
L.pop(); L.pop();
L.push(); // push nil so the caller always gets a value
} }
} }

View File

@@ -31,14 +31,13 @@ enum EventMask : uint32_t {
EVENT_ON_COLLISION = 1 << 1, EVENT_ON_COLLISION = 1 << 1,
EVENT_ON_INTERACT = 1 << 2, EVENT_ON_INTERACT = 1 << 2,
EVENT_ON_TRIGGER_ENTER = 1 << 3, EVENT_ON_TRIGGER_ENTER = 1 << 3,
EVENT_ON_TRIGGER_STAY = 1 << 4, EVENT_ON_TRIGGER_EXIT = 1 << 4,
EVENT_ON_TRIGGER_EXIT = 1 << 5, EVENT_ON_UPDATE = 1 << 5,
EVENT_ON_UPDATE = 1 << 6, EVENT_ON_DESTROY = 1 << 6,
EVENT_ON_DESTROY = 1 << 7, EVENT_ON_ENABLE = 1 << 7,
EVENT_ON_ENABLE = 1 << 8, EVENT_ON_DISABLE = 1 << 8,
EVENT_ON_DISABLE = 1 << 9, EVENT_ON_BUTTON_PRESS = 1 << 9,
EVENT_ON_BUTTON_PRESS = 1 << 10, EVENT_ON_BUTTON_RELEASE = 1 << 10,
EVENT_ON_BUTTON_RELEASE = 1 << 11,
}; };
class Lua { class Lua {
@@ -50,6 +49,8 @@ class Lua {
void LoadLuaFile(const char* code, size_t len, int index); void LoadLuaFile(const char* code, size_t len, int index);
void RegisterSceneScripts(int index); void RegisterSceneScripts(int index);
void RegisterGameObject(GameObject* go); 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 // Get the underlying psyqo::Lua state for API registration
psyqo::Lua& getState() { return m_state; } psyqo::Lua& getState() { return m_state; }
@@ -70,11 +71,12 @@ class Lua {
} }
// Event dispatchers - these check the bitmask before calling Lua // Event dispatchers - these check the bitmask before calling Lua
void OnCollision(GameObject* self, GameObject* other); void OnCollideWithPlayer(GameObject* self);
void OnInteract(GameObject* self); void OnInteract(GameObject* self);
void OnTriggerEnter(GameObject* trigger, GameObject* other); void OnTriggerEnter(GameObject* trigger, GameObject* other);
void OnTriggerStay(GameObject* trigger, GameObject* other);
void OnTriggerExit(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 OnUpdate(GameObject* go, int deltaFrames); // Per-object update
void OnDestroy(GameObject* go); void OnDestroy(GameObject* go);
void OnEnable(GameObject* go); void OnEnable(GameObject* go);
@@ -157,19 +159,18 @@ class Lua {
[[no_unique_address]] FunctionWrapper<1, typestring_is("onSceneCreationStart")> onSceneCreationStartFunctionWrapper; [[no_unique_address]] FunctionWrapper<1, typestring_is("onSceneCreationStart")> onSceneCreationStartFunctionWrapper;
[[no_unique_address]] FunctionWrapper<2, typestring_is("onSceneCreationEnd")> onSceneCreationEndFunctionWrapper; [[no_unique_address]] FunctionWrapper<2, typestring_is("onSceneCreationEnd")> onSceneCreationEndFunctionWrapper;
// Object-level events (methodId 100-111, offset to avoid collision with scene events) // Object-level events
[[no_unique_address]] FunctionWrapper<100, typestring_is("onCreate")> onCreateMethodWrapper; [[no_unique_address]] FunctionWrapper<100, typestring_is("onCreate")> onCreateMethodWrapper;
[[no_unique_address]] FunctionWrapper<101, typestring_is("onCollision")> onCollisionMethodWrapper; [[no_unique_address]] FunctionWrapper<101, typestring_is("onCollideWithPlayer")> onCollideWithPlayerMethodWrapper;
[[no_unique_address]] FunctionWrapper<102, typestring_is("onInteract")> onInteractMethodWrapper; [[no_unique_address]] FunctionWrapper<102, typestring_is("onInteract")> onInteractMethodWrapper;
[[no_unique_address]] FunctionWrapper<103, typestring_is("onTriggerEnter")> onTriggerEnterMethodWrapper; [[no_unique_address]] FunctionWrapper<103, typestring_is("onTriggerEnter")> onTriggerEnterMethodWrapper;
[[no_unique_address]] FunctionWrapper<104, typestring_is("onTriggerStay")> onTriggerStayMethodWrapper; [[no_unique_address]] FunctionWrapper<104, typestring_is("onTriggerExit")> onTriggerExitMethodWrapper;
[[no_unique_address]] FunctionWrapper<105, typestring_is("onTriggerExit")> onTriggerExitMethodWrapper; [[no_unique_address]] FunctionWrapper<105, typestring_is("onUpdate")> onUpdateMethodWrapper;
[[no_unique_address]] FunctionWrapper<106, typestring_is("onUpdate")> onUpdateMethodWrapper; [[no_unique_address]] FunctionWrapper<106, typestring_is("onDestroy")> onDestroyMethodWrapper;
[[no_unique_address]] FunctionWrapper<107, typestring_is("onDestroy")> onDestroyMethodWrapper; [[no_unique_address]] FunctionWrapper<107, typestring_is("onEnable")> onEnableMethodWrapper;
[[no_unique_address]] FunctionWrapper<108, typestring_is("onEnable")> onEnableMethodWrapper; [[no_unique_address]] FunctionWrapper<108, typestring_is("onDisable")> onDisableMethodWrapper;
[[no_unique_address]] FunctionWrapper<109, typestring_is("onDisable")> onDisableMethodWrapper; [[no_unique_address]] FunctionWrapper<109, typestring_is("onButtonPress")> onButtonPressMethodWrapper;
[[no_unique_address]] FunctionWrapper<110, typestring_is("onButtonPress")> onButtonPressMethodWrapper; [[no_unique_address]] FunctionWrapper<110, typestring_is("onButtonRelease")> onButtonReleaseMethodWrapper;
[[no_unique_address]] FunctionWrapper<111, typestring_is("onButtonRelease")> onButtonReleaseMethodWrapper;
void PushGameObject(GameObject* go); void PushGameObject(GameObject* go);
@@ -179,8 +180,16 @@ class Lua {
int m_metatableReference = LUA_NOREF; int m_metatableReference = LUA_NOREF;
int m_luascriptsReference = LUA_NOREF; int m_luascriptsReference = LUA_NOREF;
int m_luaSceneScriptsReference = LUA_NOREF; int m_luaSceneScriptsReference = LUA_NOREF;
// Event mask now stored inline in GameObject::eventMask // 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> template <int methodId, typename methodName>
friend struct FunctionWrapper; friend struct FunctionWrapper;

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,9 @@
namespace psxsplash { namespace psxsplash {
class SceneManager; // Forward declaration class SceneManager; // Forward declaration
class CutscenePlayer; // Forward declaration
class AnimationPlayer; // Forward declaration
class UISystem; // Forward declaration
/** /**
* Lua API - Provides game scripting functionality * Lua API - Provides game scripting functionality
@@ -23,7 +26,7 @@ class SceneManager; // Forward declaration
class LuaAPI { class LuaAPI {
public: public:
// Initialize all API modules // Initialize all API modules
static void RegisterAll(psyqo::Lua& L, SceneManager* scene); 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 // Called once per frame to advance the Lua frame counter
static void IncrementFrameCount(); static void IncrementFrameCount();
@@ -35,6 +38,15 @@ private:
// Store scene manager for API access // Store scene manager for API access
static SceneManager* s_sceneManager; 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 API
// ======================================================================== // ========================================================================
@@ -250,6 +262,69 @@ private:
// Reset all persistent data // Reset all persistent data
static void PersistClear(); 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 // HELPERS
// ======================================================================== // ========================================================================

View File

@@ -4,12 +4,16 @@
#include <psyqo/font.hh> #include <psyqo/font.hh>
#include <psyqo/gpu.hh> #include <psyqo/gpu.hh>
#include <psyqo/scene.hh> #include <psyqo/scene.hh>
#include <psyqo/task.hh>
#include <psyqo/trigonometry.hh> #include <psyqo/trigonometry.hh>
#include "renderer.hh" #include "renderer.hh"
#include "scenemanager.hh" #include "scenemanager.hh"
#include "sceneloader.hh" #include "fileloader.hh"
#include "pcdrv_handler.hh"
#if defined(LOADER_CDROM)
#include "fileloader_cdrom.hh"
#endif
namespace { namespace {
@@ -29,8 +33,10 @@ class MainScene final : public psyqo::Scene {
psxsplash::SceneManager m_sceneManager; psxsplash::SceneManager m_sceneManager;
// PCdrv-loaded scene data (owned) // Task queue for async FileLoader init (CD-ROM reset + ISO parse).
uint8_t* m_sceneData = nullptr; // After init completes, loadScene() handles everything synchronously.
psyqo::TaskQueue m_initQueue;
bool m_ready = false;
}; };
PSXSplash app; PSXSplash app;
@@ -48,37 +54,58 @@ void PSXSplash::prepare() {
// Initialize the Renderer singleton // Initialize the Renderer singleton
psxsplash::Renderer::Init(gpu()); psxsplash::Renderer::Init(gpu());
// Clear screen
psyqo::Prim::FastFill ff(psyqo::Color{.r = 0, .g = 0, .b = 0});
ff.rect = psyqo::Rect{0, 0, 320, 240};
gpu().sendPrimitive(ff);
ff.rect = psyqo::Rect{0, 256, 320, 240};
gpu().sendPrimitive(ff);
gpu().pumpCallbacks();
// Let the active file-loader backend do any early setup.
// CDRom: CDRomDevice::prepare() must happen here.
psxsplash::FileLoader::Get().prepare();
#if defined(LOADER_CDROM)
// The CD-ROM backend needs a GPU pointer for LoadFileSync's spin loop.
static_cast<psxsplash::FileLoaderCDRom&>(
psxsplash::FileLoader::Get()).setGPU(&gpu());
#endif
} }
void PSXSplash::createScene() { void PSXSplash::createScene() {
m_font.uploadSystemFont(gpu()); m_font.uploadSystemFont(gpu());
psxsplash::SceneManager::SetFont(&m_font);
pushScene(&mainScene); pushScene(&mainScene);
} }
void MainScene::start(StartReason reason) { void MainScene::start(StartReason reason) {
// On real hardware: register break handler for PCDRV over SIO1 + redirect printf // Initialise the FileLoader backend, then load scene 0 through
// On emulator: no-op (pcsx-redux handles PCDRV natively) // the same SceneManager::loadScene() path used for all transitions.
psxsplash::pcdrv_sio1_init(); //
// 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.
// Initialize PCdrv (break instructions - handled by emulator or our break handler) m_initQueue
psxsplash::SceneLoader::Init(); .startWith(psxsplash::FileLoader::Get().scheduleInit())
.then([this](psyqo::TaskQueue::Task* task) {
// Load the first scene via PCdrv. m_sceneManager.loadScene(gpu(), 0, /*isFirstScene=*/true);
// Files are relative to the pcdrvbase directory (PSXBuild/). m_ready = true;
int fileSize = 0; task->resolve();
m_sceneData = psxsplash::SceneLoader::LoadFile("scene_0.splashpack", fileSize); })
.butCatch([](psyqo::TaskQueue*) {
if (!m_sceneData) { // FileLoader init failed — nothing we can do on PS1.
// Fallback: try legacy name for backwards compatibility })
m_sceneData = psxsplash::SceneLoader::LoadFile("output.bin", fileSize); .run();
}
if (m_sceneData) {
m_sceneManager.InitializeScene(m_sceneData);
}
} }
void MainScene::frame() { void MainScene::frame() {
// Don't run the game loop while FileLoader init is still executing
// (only relevant for the async CD-ROM backend).
if (!m_ready) return;
uint32_t beginFrame = gpu().now(); uint32_t beginFrame = gpu().now();
auto currentFrameCounter = gpu().getFrameCount(); auto currentFrameCounter = gpu().getFrameCount();
auto deltaTime = currentFrameCounter - mainScene.m_lastFrameCounter; auto deltaTime = currentFrameCounter - mainScene.m_lastFrameCounter;
@@ -92,8 +119,10 @@ void MainScene::frame() {
m_sceneManager.GameTick(gpu()); 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", 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(); gpu().pumpCallbacks();
} }

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

@@ -24,7 +24,6 @@ namespace psxsplash {
uint16_t padding; uint16_t padding;
/// Returns true if this triangle has no texture (vertex-color only). /// Returns true if this triangle has no texture (vertex-color only).
/// These should be rendered as POLY_G3 (GouraudTriangle) instead of POLY_GT3.
bool isUntextured() const { bool isUntextured() const {
return *reinterpret_cast<const uint16_t*>(&tpage) == UNTEXTURED_TPAGE; return *reinterpret_cast<const uint16_t*>(&tpage) == UNTEXTURED_TPAGE;
} }

View File

@@ -117,40 +117,6 @@ void NavRegionSystem::closestPointOnSegment(int32_t px, int32_t pz,
outZ = az + fpmul(t, abz); outZ = az + fpmul(t, abz);
} }
// ============================================================================
// Segment crosses portal check (XZ)
// ============================================================================
bool NavRegionSystem::segmentCrossesPortal(int32_t p0x, int32_t p0z,
int32_t p1x, int32_t p1z,
int32_t ax, int32_t az,
int32_t bx, int32_t bz) {
// Standard 2D segment intersection test using cross products.
// Returns true if segment [p0,p1] crosses segment [a,b].
int32_t dx = p1x - p0x, dz = p1z - p0z;
int32_t ex = bx - ax, ez = bz - az;
int64_t denom = (int64_t)dx * ez - (int64_t)dz * ex;
if (denom == 0) return false; // Parallel
int32_t fx = ax - p0x, fz = az - p0z;
int64_t tNum = (int64_t)fx * ez - (int64_t)fz * ex;
int64_t uNum = (int64_t)fx * dz - (int64_t)fz * dx;
// Check t in [0,1] and u in [0,1]
if (denom > 0) {
if (tNum < 0 || tNum > denom) return false;
if (uNum < 0 || uNum > denom) return false;
} else {
if (tNum > 0 || tNum < denom) return false;
if (uNum > 0 || uNum < denom) return false;
}
return true;
}
// ============================================================================ // ============================================================================
// Get floor Y at position (plane equation) // Get floor Y at position (plane equation)
// ============================================================================ // ============================================================================
@@ -316,36 +282,10 @@ int32_t NavRegionSystem::resolvePosition(int32_t& newX, int32_t& newZ,
bool NavRegionSystem::findPath(uint16_t startRegion, uint16_t endRegion, bool NavRegionSystem::findPath(uint16_t startRegion, uint16_t endRegion,
NavPath& path) const { NavPath& path) const {
// STUB: Returns false until NPC pathfinding is implemented. // STUB: Returns false until NPC pathfinding is implemented.
// When implemented, this will be A* over the region adjacency graph:
// - Open set: priority queue by f-cost (g + heuristic)
// - g-cost: sum of Euclidean distances between region centroids
// - Heuristic: straight-line distance to goal centroid
// - Neighbor iteration: via portal edges
// - Max path length: NAV_MAX_PATH_STEPS
path.stepCount = 0; path.stepCount = 0;
(void)startRegion; (void)startRegion;
(void)endRegion; (void)endRegion;
return false; return false;
} }
// ============================================================================
// Get portal between two regions
// ============================================================================
const NavPortal* NavRegionSystem::getPortalBetween(uint16_t regionA, uint16_t regionB) const {
if (regionA >= m_header.regionCount) return nullptr;
const auto& reg = m_regions[regionA];
for (int i = 0; i < reg.portalCount; i++) {
uint16_t portalIdx = reg.portalStart + i;
if (portalIdx >= m_header.portalCount) break;
if (m_portals[portalIdx].neighborRegion == regionB) {
return &m_portals[portalIdx];
}
}
return nullptr;
}
} // namespace psxsplash } // namespace psxsplash

View File

@@ -9,9 +9,6 @@
* - Player has a single current region index. * - Player has a single current region index.
* - Movement: point-in-convex-polygon test → portal crossing → neighbor update. * - Movement: point-in-convex-polygon test → portal crossing → neighbor update.
* - Floor Y: project XZ onto region's floor plane. * - Floor Y: project XZ onto region's floor plane.
* - Pathfinding: A* over region adjacency graph (stub ready for NPC drop-in).
*
* All math is fixed-point 20.12. Zero floats.
*/ */
#include <stdint.h> #include <stdint.h>
@@ -106,6 +103,11 @@ public:
/// Is nav data loaded? /// Is nav data loaded?
bool isLoaded() const { return m_regions != nullptr; } 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 /// Get the number of regions
uint16_t getRegionCount() const { return m_header.regionCount; } uint16_t getRegionCount() const { return m_header.regionCount; }
@@ -143,24 +145,10 @@ public:
/// Returns the clamped position. /// Returns the clamped position.
void clampToRegion(int32_t& x, int32_t& z, uint16_t regionIndex) const; void clampToRegion(int32_t& x, int32_t& z, uint16_t regionIndex) const;
// ======================================================================== // TODO: Implement this
// Pathfinding stub — documented API for NPC drop-in
// ========================================================================
/// Find a path from startRegion to endRegion.
/// Writes region indices into path.regions[], sets path.stepCount.
/// Returns true if a path was found.
///
/// Implementation: A* over the region adjacency graph.
/// Cost heuristic: Euclidean distance between region centroids.
/// This is a STUB — returns false until NPC pathfinding is implemented.
bool findPath(uint16_t startRegion, uint16_t endRegion, bool findPath(uint16_t startRegion, uint16_t endRegion,
NavPath& path) const; NavPath& path) const;
/// Get the portal edge between two adjacent regions.
/// Returns nullptr if regions are not adjacent.
const NavPortal* getPortalBetween(uint16_t regionA, uint16_t regionB) const;
private: private:
NavDataHeader m_header = {}; NavDataHeader m_header = {};
const NavRegion* m_regions = nullptr; const NavRegion* m_regions = nullptr;
@@ -178,11 +166,6 @@ private:
int32_t bx, int32_t bz, int32_t bx, int32_t bz,
int32_t& outX, int32_t& outZ); int32_t& outX, int32_t& outZ);
/// Check if a line segment (player movement) crosses a portal edge
static bool segmentCrossesPortal(int32_t p0x, int32_t p0z,
int32_t p1x, int32_t p1z,
int32_t ax, int32_t az,
int32_t bx, int32_t bz);
}; };
} // namespace psxsplash } // namespace psxsplash

View File

@@ -1,26 +1,7 @@
/*
* pcdrv_handler.hh - Unified PCDRV API with runtime dispatch
*
* On pcsx-redux (emulator), uses pcdrv.h break instructions which are
* intercepted at the CPU level natively.
*
* On real hardware, bypasses break instructions entirely and communicates
* directly over SIO1 using the same protocol as PCdrvSerialHost.cs.
* This avoids reliance on the exception save area, which is fragile
* across different compiler versions and optimization levels.
*
* Additionally, redirects PSYQo's printf output to SIO1 on real hardware.
*
* Call pcdrv_sio1_init() once at startup, after PSYQo initialization.
* Then use pcdrv_open/read/write/close/seek instead of PCopen/PCread/etc.
*/
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
#include <stdarg.h>
#include <psyqo/kernel.hh> #include <psyqo/kernel.hh>
#include <psyqo/xprintf.h>
#include "common/hardware/pcsxhw.h" #include "common/hardware/pcsxhw.h"
#include "common/kernel/pcdrv.h" #include "common/kernel/pcdrv.h"
@@ -79,18 +60,6 @@ static inline void sio_pcdrv_escape(uint32_t funcCode) {
sio_write32(funcCode); sio_write32(funcCode);
} }
// =========================================================================
// Runtime detection - reads magic at 0x1F802080 each call.
// NOT cached in a static, because this is a header-only file and each
// translation unit would get its own copy of any static variable.
// pcsx_present() is a single bus read - negligible cost.
// =========================================================================
// =========================================================================
// Direct SIO1 PCDRV implementations (real hardware path)
// These call the host protocol directly with actual pointers/values,
// bypassing break instructions and the exception save area entirely.
// =========================================================================
static int sio_pcdrv_init() { static int sio_pcdrv_init() {
sio_pcdrv_escape(0x101); sio_pcdrv_escape(0x101);
@@ -114,19 +83,6 @@ static int sio_pcdrv_open(const char* name, int flags) {
return -1; return -1;
} }
static int sio_pcdrv_creat(const char* name) {
sio_pcdrv_escape(0x102);
if (!sio_check_okay()) return -1;
const char* p = name;
while (*p) sio_putc((uint8_t)*p++);
sio_putc(0x00);
sio_write32(0); // params
if (sio_check_okay()) {
return (int)sio_read32(); // handle
}
return -1;
}
static int sio_pcdrv_close(int fd) { static int sio_pcdrv_close(int fd) {
sio_pcdrv_escape(0x104); sio_pcdrv_escape(0x104);
if (!sio_check_okay()) return -1; if (!sio_check_okay()) return -1;
@@ -158,23 +114,6 @@ static int sio_pcdrv_read(int fd, void* buf, int len) {
return -1; return -1;
} }
static int sio_pcdrv_write(int fd, const void* buf, int len) {
sio_pcdrv_escape(0x106);
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()) return -1;
const uint8_t* src = (const uint8_t*)buf;
for (int i = 0; i < len; i++) {
sio_putc(src[i]);
}
if (sio_check_okay()) {
return (int)sio_read32(); // bytes written
}
return -1;
}
static int sio_pcdrv_seek(int fd, int offset, int whence) { static int sio_pcdrv_seek(int fd, int offset, int whence) {
sio_pcdrv_escape(0x107); sio_pcdrv_escape(0x107);
if (!sio_check_okay()) return -1; if (!sio_check_okay()) return -1;
@@ -202,11 +141,6 @@ static int pcdrv_open(const char* name, int flags, int perms) {
return sio_pcdrv_open(name, flags); return sio_pcdrv_open(name, flags);
} }
static int pcdrv_creat(const char* name, int perms) {
if (pcsx_present()) return PCcreat(name, perms);
return sio_pcdrv_creat(name);
}
static int pcdrv_close(int fd) { static int pcdrv_close(int fd) {
if (pcsx_present()) return PCclose(fd); if (pcsx_present()) return PCclose(fd);
return sio_pcdrv_close(fd); return sio_pcdrv_close(fd);
@@ -217,11 +151,6 @@ static int pcdrv_read(int fd, void* buf, int len) {
return sio_pcdrv_read(fd, buf, len); return sio_pcdrv_read(fd, buf, len);
} }
static int pcdrv_write(int fd, const void* buf, int len) {
if (pcsx_present()) return PCwrite(fd, buf, len);
return sio_pcdrv_write(fd, buf, len);
}
static int pcdrv_seek(int fd, int offset, int whence) { static int pcdrv_seek(int fd, int offset, int whence) {
if (pcsx_present()) return PClseek(fd, offset, whence); if (pcsx_present()) return PClseek(fd, offset, whence);
return sio_pcdrv_seek(fd, offset, whence); return sio_pcdrv_seek(fd, offset, whence);
@@ -239,51 +168,6 @@ static void sio1Init() {
for (int i = 0; i < 100; i++) { __asm__ volatile("" ::: "memory"); } // settle delay for (int i = 0; i < 100; i++) { __asm__ volatile("" ::: "memory"); } // settle delay
} }
// =========================================================================
// Printf redirect - replaces PSYQo's printfStub with SIO1 output
//
// PSYQo's kernel takeover (takeOverKernel) destroys the BIOS and replaces
// the A0/B0/C0 jump handlers. Only A0[0x3F] (printf) is functional; all
// other BIOS calls return immediately. PSYQo's printfStub calls
// syscall_write(1,...) which goes to A0[0x03] - a dead no-op on real HW.
//
// Fix: replace the printf target address embedded in the A0 handler code
// at addresses 0xa8 (lui $t0, hi) and 0xb4 (ori $t0, $t0, lo) with our
// function that outputs directly to SIO1.
// =========================================================================
// Printf replacement that sends output to SIO1
static int sio1Printf(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
int r = vxprintf([](const char* data, int size, void*) {
for (int i = 0; i < size; i++) {
while (!(SIO1_STAT & SIO1_TX_RDY)) {}
SIO1_DATA = (uint8_t)data[i];
}
}, nullptr, fmt, args);
va_end(args);
return r;
}
static void redirectPrintfToSIO1() {
uintptr_t addr = (uintptr_t)sio1Printf;
uint16_t hi = (uint16_t)(addr >> 16);
uint16_t lo = (uint16_t)(addr & 0xffff);
if (lo >= 0x8000) hi++; // sign-extension compensation for ori
// Patch the A0 handler's embedded address:
// 0xa8: lui $t0, hi (opcode 001111, rs=0, rt=$t0=$8)
// 0xb4: ori $t0, $t0, lo (opcode 001101, rs=$t0, rt=$t0)
*(volatile uint32_t*)0xa8 = 0x3C080000 | hi; // lui $t0, hi
*(volatile uint32_t*)0xb4 = 0x35080000 | lo; // ori $t0, $t0, lo
psyqo::Kernel::flushCache();
}
// =========================================================================
// Master init - call once at startup, after PSYQo initialization
// =========================================================================
static void pcdrv_sio1_init() { static void pcdrv_sio1_init() {
if (pcsx_present()) return; // emulator handles PCDRV natively if (pcsx_present()) return; // emulator handles PCDRV natively

View File

@@ -16,6 +16,10 @@
#include <psyqo/vector.hh> #include <psyqo/vector.hh>
#include "gtemath.hh" #include "gtemath.hh"
#include "uisystem.hh"
#ifdef PSXSPLASH_MEMOVERLAY
#include "memoverlay.hh"
#endif
using namespace psyqo::fixed_point_literals; using namespace psyqo::fixed_point_literals;
using namespace psyqo::trig_literals; using namespace psyqo::trig_literals;
@@ -41,24 +45,18 @@ void psxsplash::Renderer::SetCamera(psxsplash::Camera& camera) { m_currentCamera
void psxsplash::Renderer::SetFog(const FogConfig& fog) { void psxsplash::Renderer::SetFog(const FogConfig& fog) {
m_fog = fog; m_fog = fog;
// Always use fog color as the GPU clear/back color
m_clearcolor = fog.color;
if (fog.enabled) { if (fog.enabled) {
m_clearcolor = fog.color;
write<Register::RFC, Unsafe>(static_cast<uint32_t>(fog.color.r) << 4); write<Register::RFC, Unsafe>(static_cast<uint32_t>(fog.color.r) << 4);
write<Register::GFC, Unsafe>(static_cast<uint32_t>(fog.color.g) << 4); write<Register::GFC, Unsafe>(static_cast<uint32_t>(fog.color.g) << 4);
write<Register::BFC, Safe>(static_cast<uint32_t>(fog.color.b) << 4); write<Register::BFC, Safe>(static_cast<uint32_t>(fog.color.b) << 4);
m_fog.fogFarSZ = 8000 / fog.density; m_fog.fogFarSZ = 8000 / fog.density;
} else { } else {
m_clearcolor = {.r = 0, .g = 0, .b = 0};
m_fog.fogFarSZ = 0; m_fog.fogFarSZ = 0;
} }
} }
void psxsplash::Renderer::writeFogRegisters() {
// Per-vertex fog is now computed manually in processTriangle (no DPCT).
// DQA/DQB/RFC/GFC/BFC are no longer needed for fog.
// The fog color is used directly via m_fog.color in the fogBlend function.
}
psyqo::Vec3 psxsplash::Renderer::computeCameraViewPos() { psyqo::Vec3 psxsplash::Renderer::computeCameraViewPos() {
::clear<Register::TRX, Safe>(); ::clear<Register::TRX, Safe>();
::clear<Register::TRY, Safe>(); ::clear<Register::TRY, Safe>();
@@ -137,9 +135,8 @@ void psxsplash::Renderer::processTriangle(
int32_t fogIR[3] = {0, 0, 0}; int32_t fogIR[3] = {0, 0, 0};
if (fogFarSZ > 0) { if (fogFarSZ > 0) {
int32_t fogNear = fogFarSZ / 4; int32_t fogNear = fogFarSZ / 4;
int32_t range4 = (fogFarSZ - fogNear) >> 4; int32_t range = fogFarSZ - fogNear;
if (range4 < 1) range4 = 1; if (range < 1) range = 1;
int32_t scale = 4096 / range4;
int32_t szArr[3] = {sz0, sz1, sz2}; int32_t szArr[3] = {sz0, sz1, sz2};
for (int vi = 0; vi < 3; vi++) { for (int vi = 0; vi < 3; vi++) {
int32_t ir; int32_t ir;
@@ -148,11 +145,10 @@ void psxsplash::Renderer::processTriangle(
} else if (szArr[vi] >= fogFarSZ) { } else if (szArr[vi] >= fogFarSZ) {
ir = 4096; ir = 4096;
} else { } else {
ir = ((szArr[vi] - fogNear) * scale) >> 4; // Linear 0..4096 over [fogNear, fogFarSZ]
if (ir > 4096) ir = 4096; int32_t t = ((szArr[vi] - fogNear) * 4096) / range;
int32_t inv = 4096 - ir; // Quadratic ease-in: t^2 / 4096
ir = 4096 - ((inv >> 2) * (inv >> 2) >> 8); ir = (t * t) >> 12;
if (ir < 0) ir = 0;
} }
fogIR[vi] = ir; fogIR[vi] = ir;
} }
@@ -266,16 +262,25 @@ void psxsplash::Renderer::Render(eastl::vector<GameObject*>& objects) {
auto& ditherCmd = balloc.allocateFragment<psyqo::Prim::TPage>(); auto& ditherCmd = balloc.allocateFragment<psyqo::Prim::TPage>();
ditherCmd.primitive.attr.setDithering(true); ditherCmd.primitive.attr.setDithering(true);
ot.insert(ditherCmd, ORDERING_TABLE_SIZE - 1); ot.insert(ditherCmd, ORDERING_TABLE_SIZE - 1);
writeFogRegisters();
psyqo::Vec3 cameraPosition = computeCameraViewPos(); psyqo::Vec3 cameraPosition = computeCameraViewPos();
int32_t fogFarSZ = m_fog.fogFarSZ; int32_t fogFarSZ = m_fog.fogFarSZ;
for (auto& obj : objects) { for (auto& obj : objects) {
if (!obj->isActive()) continue;
setupObjectTransform(obj, cameraPosition); setupObjectTransform(obj, cameraPosition);
for (int i = 0; i < obj->polyCount; i++) for (int i = 0; i < obj->polyCount; i++)
processTriangle(obj->polygons[i], fogFarSZ, ot, balloc); processTriangle(obj->polygons[i], fogFarSZ, ot, balloc);
} }
if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc);
#ifdef PSXSPLASH_MEMOVERLAY
if (m_memOverlay) m_memOverlay->renderOT(ot, balloc);
#endif
m_gpu.getNextClear(clear.primitive, m_clearcolor); m_gpu.getNextClear(clear.primitive, m_clearcolor);
m_gpu.chain(clear); m_gpu.chain(ot); m_gpu.chain(clear); m_gpu.chain(ot);
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
#ifdef PSXSPLASH_MEMOVERLAY
if (m_memOverlay) m_memOverlay->renderText(m_gpu);
#endif
m_frameCount++; m_frameCount++;
} }
@@ -288,7 +293,7 @@ void psxsplash::Renderer::RenderWithBVH(eastl::vector<GameObject*>& objects, con
auto& ditherCmd2 = balloc.allocateFragment<psyqo::Prim::TPage>(); auto& ditherCmd2 = balloc.allocateFragment<psyqo::Prim::TPage>();
ditherCmd2.primitive.attr.setDithering(true); ditherCmd2.primitive.attr.setDithering(true);
ot.insert(ditherCmd2, ORDERING_TABLE_SIZE - 1); ot.insert(ditherCmd2, ORDERING_TABLE_SIZE - 1);
writeFogRegisters();
Frustum frustum; m_currentCamera->ExtractFrustum(frustum); Frustum frustum; m_currentCamera->ExtractFrustum(frustum);
int visibleCount = bvh.cullFrustum(frustum, m_visibleRefs, MAX_VISIBLE_TRIANGLES); int visibleCount = bvh.cullFrustum(frustum, m_visibleRefs, MAX_VISIBLE_TRIANGLES);
psyqo::Vec3 cameraPosition = computeCameraViewPos(); psyqo::Vec3 cameraPosition = computeCameraViewPos();
@@ -298,6 +303,7 @@ void psxsplash::Renderer::RenderWithBVH(eastl::vector<GameObject*>& objects, con
const TriangleRef& ref = m_visibleRefs[i]; const TriangleRef& ref = m_visibleRefs[i];
if (ref.objectIndex >= objects.size()) continue; if (ref.objectIndex >= objects.size()) continue;
GameObject* obj = objects[ref.objectIndex]; GameObject* obj = objects[ref.objectIndex];
if (!obj->isActive()) continue;
if (ref.triangleIndex >= obj->polyCount) continue; if (ref.triangleIndex >= obj->polyCount) continue;
if (ref.objectIndex != lastObjectIndex) { if (ref.objectIndex != lastObjectIndex) {
lastObjectIndex = ref.objectIndex; lastObjectIndex = ref.objectIndex;
@@ -305,8 +311,16 @@ void psxsplash::Renderer::RenderWithBVH(eastl::vector<GameObject*>& objects, con
} }
processTriangle(obj->polygons[ref.triangleIndex], fogFarSZ, ot, balloc); processTriangle(obj->polygons[ref.triangleIndex], fogFarSZ, ot, balloc);
} }
if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc);
#ifdef PSXSPLASH_MEMOVERLAY
if (m_memOverlay) m_memOverlay->renderOT(ot, balloc);
#endif
m_gpu.getNextClear(clear.primitive, m_clearcolor); m_gpu.getNextClear(clear.primitive, m_clearcolor);
m_gpu.chain(clear); m_gpu.chain(ot); m_gpu.chain(clear); m_gpu.chain(ot);
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
#ifdef PSXSPLASH_MEMOVERLAY
if (m_memOverlay) m_memOverlay->renderText(m_gpu);
#endif
m_frameCount++; m_frameCount++;
} }
@@ -488,7 +502,7 @@ void psxsplash::Renderer::RenderWithRooms(eastl::vector<GameObject*>& objects,
auto& ditherCmd3 = balloc.allocateFragment<psyqo::Prim::TPage>(); auto& ditherCmd3 = balloc.allocateFragment<psyqo::Prim::TPage>();
ditherCmd3.primitive.attr.setDithering(true); ditherCmd3.primitive.attr.setDithering(true);
ot.insert(ditherCmd3, ORDERING_TABLE_SIZE - 1); ot.insert(ditherCmd3, ORDERING_TABLE_SIZE - 1);
writeFogRegisters();
psyqo::Vec3 cameraPosition = computeCameraViewPos(); psyqo::Vec3 cameraPosition = computeCameraViewPos();
int32_t fogFarSZ = m_fog.fogFarSZ; int32_t fogFarSZ = m_fog.fogFarSZ;
int32_t camX = m_currentCamera->GetPosition().x.raw(); int32_t camX = m_currentCamera->GetPosition().x.raw();
@@ -528,6 +542,7 @@ void psxsplash::Renderer::RenderWithRooms(eastl::vector<GameObject*>& objects,
const TriangleRef& ref = roomTriRefs[rm.firstTriRef + ti]; const TriangleRef& ref = roomTriRefs[rm.firstTriRef + ti];
if (ref.objectIndex >= objects.size()) continue; if (ref.objectIndex >= objects.size()) continue;
GameObject* obj = objects[ref.objectIndex]; GameObject* obj = objects[ref.objectIndex];
if (!obj->isActive()) continue;
if (ref.triangleIndex >= obj->polyCount) continue; if (ref.triangleIndex >= obj->polyCount) continue;
if (ref.objectIndex != lastObj) { lastObj = ref.objectIndex; setupObjectTransform(obj, cameraPosition); } if (ref.objectIndex != lastObj) { lastObj = ref.objectIndex; setupObjectTransform(obj, cameraPosition); }
processTriangle(obj->polygons[ref.triangleIndex], fogFarSZ, ot, balloc); processTriangle(obj->polygons[ref.triangleIndex], fogFarSZ, ot, balloc);
@@ -747,8 +762,16 @@ void psxsplash::Renderer::RenderWithRooms(eastl::vector<GameObject*>& objects,
} }
#endif #endif
if (m_uiSystem) m_uiSystem->renderOT(m_gpu, ot, balloc);
#ifdef PSXSPLASH_MEMOVERLAY
if (m_memOverlay) m_memOverlay->renderOT(ot, balloc);
#endif
m_gpu.getNextClear(clear.primitive, m_clearcolor); m_gpu.getNextClear(clear.primitive, m_clearcolor);
m_gpu.chain(clear); m_gpu.chain(ot); m_gpu.chain(clear); m_gpu.chain(ot);
if (m_uiSystem) m_uiSystem->renderText(m_gpu);
#ifdef PSXSPLASH_MEMOVERLAY
if (m_memOverlay) m_memOverlay->renderText(m_gpu);
#endif
m_frameCount++; m_frameCount++;
} }

View File

@@ -20,6 +20,11 @@
namespace psxsplash { namespace psxsplash {
class UISystem; // Forward declaration
#ifdef PSXSPLASH_MEMOVERLAY
class MemOverlay; // Forward declaration
#endif
struct FogConfig { struct FogConfig {
bool enabled = false; bool enabled = false;
psyqo::Color color = {.r = 0, .g = 0, .b = 0}; psyqo::Color color = {.r = 0, .g = 0, .b = 0};
@@ -32,8 +37,14 @@ class Renderer final {
Renderer(const Renderer&) = delete; Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete; Renderer& operator=(const Renderer&) = delete;
static constexpr size_t ORDERING_TABLE_SIZE = 2048 * 8; #ifndef OT_SIZE
static constexpr size_t BUMP_ALLOCATOR_SIZE = 8096 * 24; #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 size_t MAX_VISIBLE_TRIANGLES = 4096;
static constexpr int32_t PROJ_H = 120; static constexpr int32_t PROJ_H = 120;
@@ -55,6 +66,12 @@ class Renderer final {
void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY, void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY,
int16_t width, int16_t height); int16_t width, int16_t height);
void SetUISystem(UISystem* ui) { m_uiSystem = ui; }
#ifdef PSXSPLASH_MEMOVERLAY
void SetMemOverlay(MemOverlay* overlay) { m_memOverlay = overlay; }
#endif
psyqo::GPU& getGPU() { return m_gpu; }
static Renderer& GetInstance() { static Renderer& GetInstance() {
psyqo::Kernel::assert(instance != nullptr, psyqo::Kernel::assert(instance != nullptr,
"Access to renderer was tried without prior initialization"); "Access to renderer was tried without prior initialization");
@@ -78,15 +95,17 @@ class Renderer final {
FogConfig m_fog; FogConfig m_fog;
psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0}; psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0};
UISystem* m_uiSystem = nullptr;
#ifdef PSXSPLASH_MEMOVERLAY
MemOverlay* m_memOverlay = nullptr;
#endif
TriangleRef m_visibleRefs[MAX_VISIBLE_TRIANGLES]; TriangleRef m_visibleRefs[MAX_VISIBLE_TRIANGLES];
int m_frameCount = 0; int m_frameCount = 0;
void writeFogRegisters();
psyqo::Vec3 computeCameraViewPos(); psyqo::Vec3 computeCameraViewPos();
void setupObjectTransform(GameObject* obj, const psyqo::Vec3& cameraPosition); void setupObjectTransform(GameObject* obj, const psyqo::Vec3& cameraPosition);
// Core triangle pipeline: rtpt -> nclip -> screen-space clip -> emit.
// Uses Bandwidth's proven approach: nclip always, max-SZ depth, screen clip.
void processTriangle(Tri& tri, int32_t fogFarSZ, void processTriangle(Tri& tri, int32_t fogFarSZ,
psyqo::OrderingTable<ORDERING_TABLE_SIZE>& ot, psyqo::OrderingTable<ORDERING_TABLE_SIZE>& ot,
psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE>& balloc); psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE>& balloc);

View File

@@ -1,65 +0,0 @@
#include "sceneloader.hh"
// Unified PCDRV API with runtime dispatch:
// - Emulator: break instructions (intercepted by pcsx-redux)
// - Real hardware: direct SIO1 protocol (no break instructions)
#include "pcdrv_handler.hh"
namespace psxsplash {
bool SceneLoader::s_pcdrvAvailable = false;
bool SceneLoader::Init() {
s_pcdrvAvailable = (pcdrv_init() == 0);
return s_pcdrvAvailable;
}
bool SceneLoader::IsPCdrvAvailable() {
return s_pcdrvAvailable;
}
uint8_t* SceneLoader::LoadFile(const char* filename, int& outSize) {
outSize = 0;
if (!s_pcdrvAvailable) {
return nullptr;
}
// Open the file (read-only, flags=0, perms=0)
int fd = pcdrv_open(filename, 0, 0);
if (fd < 0) {
return nullptr;
}
// Get file size by seeking to end
int size = pcdrv_seek(fd, 0, 2); // SEEK_END = 2
if (size <= 0) {
pcdrv_close(fd);
return nullptr;
}
// Seek back to start
pcdrv_seek(fd, 0, 0); // SEEK_SET = 0
// Allocate buffer (aligned to 4 bytes for struct casting)
int alignedSize = (size + 3) & ~3;
uint8_t* buffer = new uint8_t[alignedSize];
// Read the file
int bytesRead = pcdrv_read(fd, buffer, size);
pcdrv_close(fd);
if (bytesRead != size) {
delete[] buffer;
return nullptr;
}
outSize = size;
return buffer;
}
void SceneLoader::FreeFile(uint8_t* data) {
delete[] data;
}
} // namespace psxsplash

View File

@@ -1,49 +0,0 @@
#pragma once
#include <stdint.h>
namespace psxsplash {
/**
* SceneLoader — loads splashpack files from PCdrv (emulator) or CD-ROM.
*
* In emulator (PCdrv) mode, files are loaded via the host filesystem using
* the PCdrv protocol (break instructions intercepted by PCSX-Redux).
*
* In CD-ROM mode (future), files would be loaded from the disc.
*
* The loader allocates memory for the file content and returns a pointer
* to the caller. The caller owns the memory.
*/
class SceneLoader {
public:
/**
* Initialize the loader. Must be called once at startup.
* Returns true if PCdrv is available, false otherwise.
*/
static bool Init();
/**
* Load a file by name. Returns a pointer to the loaded data.
* The data is allocated with new[] and the caller owns it.
* @param filename The filename to load (relative to pcdrvbase).
* @param outSize Receives the file size in bytes.
* @return Pointer to loaded data, or nullptr on failure.
*/
static uint8_t* LoadFile(const char* filename, int& outSize);
/**
* Free previously loaded file data.
*/
static void FreeFile(uint8_t* data);
/**
* Returns true if PCdrv is available.
*/
static bool IsPCdrvAvailable();
private:
static bool s_pcdrvAvailable;
};
} // namespace psxsplash

File diff suppressed because it is too large Load Diff

View File

@@ -13,23 +13,35 @@
#include "gameobject.hh" #include "gameobject.hh"
#include "lua.h" #include "lua.h"
#include "splashpack.hh" #include "splashpack.hh"
#include "worldcollision.hh"
#include "navregion.hh" #include "navregion.hh"
#include "audiomanager.hh" #include "audiomanager.hh"
#include "interactable.hh" #include "interactable.hh"
#include "luaapi.hh" #include "luaapi.hh"
#include "sceneloader.hh" #include "fileloader.hh"
#include "cutscene.hh"
#include "animation.hh"
#include "uisystem.hh"
#ifdef PSXSPLASH_MEMOVERLAY
#include "memoverlay.hh"
#endif
namespace psxsplash { namespace psxsplash {
// Forward-declare; full definition in loadingscreen.hh
class LoadingScreen;
class SceneManager { class SceneManager {
public: public:
void InitializeScene(uint8_t* splashpackData); void InitializeScene(uint8_t* splashpackData, LoadingScreen* loading = nullptr);
void GameTick(psyqo::GPU &gpu); void GameTick(psyqo::GPU &gpu);
// Trigger event callbacks (called by CollisionSystem) // Font access (set from main.cpp after uploadSystemFont)
void fireTriggerEnter(uint16_t triggerObjIdx, uint16_t otherObjIdx); static void SetFont(psyqo::Font<>* font) { s_font = font; }
void fireTriggerStay(uint16_t triggerObjIdx, uint16_t otherObjIdx); static psyqo::Font<>* GetFont() { return s_font; }
void fireTriggerExit(uint16_t triggerObjIdx, uint16_t otherObjIdx);
// 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) // Get game object by index (for collision callbacks)
GameObject* getGameObject(uint16_t index) { GameObject* getGameObject(uint16_t index) {
@@ -70,11 +82,30 @@ class SceneManager {
Camera& getCamera() { return m_currentCamera; } Camera& getCamera() { return m_currentCamera; }
Lua& getLua() { return L; } Lua& getLua() { return L; }
AudioManager& getAudio() { return m_audio; } 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 via PCdrv) // Scene loading (for multi-scene support)
void requestSceneLoad(int sceneIndex); void requestSceneLoad(int sceneIndex);
int getCurrentSceneIndex() const { return m_currentSceneIndex; } 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) // Check and process pending scene load (called from GameTick)
void processPendingSceneLoad(); void processPendingSceneLoad();
@@ -83,7 +114,6 @@ class SceneManager {
psxsplash::SplashPackLoader m_loader; psxsplash::SplashPackLoader m_loader;
CollisionSystem m_collisionSystem; CollisionSystem m_collisionSystem;
BVHManager m_bvh; // Spatial acceleration for frustum culling BVHManager m_bvh; // Spatial acceleration for frustum culling
WorldCollision m_worldCollision; // Triangle-level world collision (v7+)
NavRegionSystem m_navRegions; // Convex region navigation (v7+) NavRegionSystem m_navRegions; // Convex region navigation (v7+)
uint16_t m_playerNavRegion = NAV_NO_REGION; // Current nav region for player uint16_t m_playerNavRegion = NAV_NO_REGION; // Current nav region for player
@@ -113,6 +143,20 @@ class SceneManager {
// Audio system // Audio system
AudioManager m_audio; 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::Controls m_controls;
psxsplash::Camera m_currentCamera; psxsplash::Camera m_currentCamera;
@@ -122,28 +166,33 @@ class SceneManager {
psyqo::FixedPoint<12, uint16_t> m_playerHeight; psyqo::FixedPoint<12, uint16_t> m_playerHeight;
// Movement physics (v8+) int32_t m_playerRadius;
int32_t m_playerRadius; // Collision radius in fp12 (replaces hardcoded PLAYER_RADIUS) int32_t m_velocityY;
int32_t m_velocityY; // Vertical velocity in fp12 per second (negative = up) int32_t m_gravityPerFrame;
int32_t m_gravityPerFrame; // Gravity velocity change per frame (fp12) int32_t m_jumpVelocityRaw;
int32_t m_jumpVelocityRaw; // Initial jump velocity in fp12 per second bool m_isGrounded;
bool m_isGrounded; // On the ground (can jump)
// Frame timing // Frame timing
uint32_t m_lastFrameTime; // gpu.now() timestamp of previous frame uint32_t m_lastFrameTime; // gpu.now() timestamp of previous frame
int m_deltaFrames; // Elapsed frame count (1 normally, 2+ if dropped) int m_deltaFrames; // Elapsed frame count (1 normally, 2+ if dropped)
bool freecam = false; 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 // Scene transition state
int m_currentSceneIndex = 0; int m_currentSceneIndex = 0;
int m_pendingSceneIndex = -1; // -1 = no pending load int m_pendingSceneIndex = -1; // -1 = no pending load
uint8_t* m_currentSceneData = nullptr; // Owned pointer to loaded data 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) // System update methods (called from GameTick)
void updateInteractionSystem(); void updateInteractionSystem();
void processEnableDisableEvents(); void processEnableDisableEvents();
void clearScene(); // Deallocate current scene objects void clearScene(); // Deallocate current scene objects
void shrinkBuffer(); // Free pixel/audio bulk data after VRAM/SPU uploads
}; };
}; // namespace psxsplash } // namespace psxsplash
// namespace psxsplash

View File

@@ -1,296 +0,0 @@
/*
* sio_pcdrv.h — SIO1-based PCDrv implementation for PSYQo applications
*
* Problem: PSYQo's kernel initialization overwrites the exception handler
* at 0x80000080, destroying Unirom's DEBG hooks. The standard
* pcdrv.h functions use MIPS `break` instructions that rely on
* those hooks to translate into SIO escape sequences.
*
* Solution: Bypass the `break` instruction mechanism entirely. Instead,
* talk directly to SIO1 hardware and send the exact same 0x00+'p'
* escape protocol that the host (NOTPSXSerial / PCdrvSerialHost)
* expects. This works regardless of what's at the exception vector.
*
* Protocol: Matches NOTPSXSerial's PCDrv.cs / Bridge.MonitorSerial():
* PS1 → Host: 0x00 'p' funcCode(4 LE)
* Host → PS1: "OKAY" ... (function-specific data)
* or "NOPE" on error
*/
#pragma once
#include <stdint.h>
// ═══════════════════════════════════════════════════════════════════════
// SIO1 hardware registers (UART serial port, 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)
// Status register bits
#define SIO1_STAT_TX_RDY (1 << 0) // TX FIFO not full
#define SIO1_STAT_RX_RDY (1 << 1) // RX data available
// ═══════════════════════════════════════════════════════════════════════
// Low-level SIO1 I/O — blocking, tight polling loops
// ═══════════════════════════════════════════════════════════════════════
static inline void sio_putc(uint8_t byte) {
while (!(SIO1_STAT & SIO1_STAT_TX_RDY)) {}
SIO1_DATA = byte;
}
static inline uint8_t sio_getc() {
while (!(SIO1_STAT & SIO1_STAT_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 void sio_send_str(const char *s) {
while (*s) sio_putc((uint8_t)*s++);
}
// Read 4 bytes and check if they are "OKAY"
static inline int sio_check_okay() {
uint8_t a = sio_getc();
uint8_t b = sio_getc();
uint8_t c = sio_getc();
uint8_t d = sio_getc();
return (a == 'O' && b == 'K' && c == 'A' && d == 'Y');
}
// ═══════════════════════════════════════════════════════════════════════
// PCDrv escape protocol — send 0x00 + 'p' + function code
// ═══════════════════════════════════════════════════════════════════════
static inline void sio_pcdrv_escape(uint32_t funcCode) {
sio_putc(0x00); // escape character
sio_putc('p'); // PCDrv marker
sio_write32(funcCode); // function code, little-endian
}
// ═══════════════════════════════════════════════════════════════════════
// PCDrv API — drop-in replacements for common/kernel/pcdrv.h
// Same names, same signatures, same return conventions.
// ═══════════════════════════════════════════════════════════════════════
/**
* sio1_ensure_init — (re-)initialize SIO1 for 115200 8N1
* Safe to call multiple times. Uses the same register values
* that Unirom/nugget use, so this is a no-op if SIO1 is already
* configured. Ensures correct config even if PSYQo or BIOS
* touched the SIO1 registers.
*/
static inline void sio1_ensure_init() {
SIO1_CTRL = 0; // reset
SIO1_MODE = 0x004e; // MUL16, 8 data bits, no parity, 1 stop bit
SIO1_BAUD = (uint16_t)(2073600 / 115200); // = 18
SIO1_CTRL = 0x0025; // TX enable, RX enable, RTS assert
// Small delay for hardware to settle
{
int i = 0;
while (i < 100) { __asm__ volatile("" ::: "memory"); i++; }
}
}
/**
* PCinit — initialize PCDrv connection
* Returns 0 on success, -1 on failure.
*/
static inline int PCinit() {
sio1_ensure_init(); // make sure SIO1 is properly configured
sio_pcdrv_escape(0x101);
// Host responds: "OKAY" + 0x00
if (!sio_check_okay()) return -1;
sio_getc(); // consume trailing 0x00
return 0;
}
/**
* PCopen — open a file on the host
* Returns file handle (positive) on success, -1 on failure.
*/
static inline int PCopen(const char *name, int flags, int perms) {
(void)perms; // unused in protocol
sio_pcdrv_escape(0x103);
// Host responds: "OKAY" (first ACK, ready for filename)
if (!sio_check_okay()) return -1;
// Send filename (null-terminated)
const char *p = name;
while (*p) sio_putc((uint8_t)*p++);
sio_putc(0x00); // null terminator
// Send file mode as uint32 LE
sio_write32((uint32_t)flags);
// Host responds: "OKAY" + handle(4) or "NOPE"
uint8_t r0 = sio_getc();
uint8_t r1 = sio_getc();
uint8_t r2 = sio_getc();
uint8_t r3 = sio_getc();
if (r0 == 'N' && r1 == 'O' && r2 == 'P' && r3 == 'E') {
return -1;
}
// "OKAY" — read handle
int handle = (int)sio_read32();
return handle;
}
/**
* PCclose — close a file handle
* Returns 0 on success.
*/
static inline int PCclose(int fd) {
sio_pcdrv_escape(0x104);
// Host responds: "OKAY" (ready for params)
if (!sio_check_okay()) return -1;
// Send handle + 2 unused params (matches Unirom kernel convention)
sio_write32((uint32_t)fd);
sio_write32(0); // unused
sio_write32(0); // unused
// Host responds: "OKAY" + handle(4) or "NOPE"
uint8_t r0 = sio_getc();
uint8_t r1 = sio_getc();
uint8_t r2 = sio_getc();
uint8_t r3 = sio_getc();
if (r0 == 'N' && r1 == 'O' && r2 == 'P' && r3 == 'E') {
return -1;
}
// "OKAY" — read handle back (v1, not used by caller)
sio_read32();
return 0;
}
/**
* PCread — read data from a file into memory
* Returns number of bytes read, or -1 on failure.
*/
static inline int PCread(int fd, void *buf, int len) {
sio_pcdrv_escape(0x105);
// Host responds: "OKAY" (ready for params)
if (!sio_check_okay()) return -1;
// Send handle + length + memaddr (memaddr is debug-only, send buf ptr)
sio_write32((uint32_t)fd);
sio_write32((uint32_t)len);
sio_write32((uint32_t)(uintptr_t)buf);
// Host responds: "OKAY" + dataLength(4) + checksum(4) + raw data
// or "NOPE"
uint8_t r0 = sio_getc();
uint8_t r1 = sio_getc();
uint8_t r2 = sio_getc();
uint8_t r3 = sio_getc();
if (r0 == 'N' && r1 == 'O' && r2 == 'P' && r3 == 'E') {
return -1;
}
// "OKAY" — read response
uint32_t dataLength = sio_read32();
uint32_t checksum = sio_read32(); // not verified, just consume
(void)checksum;
// Read raw data bytes into buffer
uint8_t *dst = (uint8_t *)buf;
for (uint32_t i = 0; i < dataLength; i++) {
dst[i] = sio_getc();
}
return (int)dataLength;
}
/**
* PCwrite — write data from memory to a file
* Returns number of bytes written, or -1 on failure.
*/
static inline int PCwrite(int fd, const void *buf, int len) {
sio_pcdrv_escape(0x106);
// Host responds: "OKAY" (ready for params)
if (!sio_check_okay()) return -1;
// Send handle + length + memaddr
sio_write32((uint32_t)fd);
sio_write32((uint32_t)len);
sio_write32((uint32_t)(uintptr_t)buf);
// Host responds: "OKAY" (ready for data) or "NOPE"
uint8_t r0 = sio_getc();
uint8_t r1 = sio_getc();
uint8_t r2 = sio_getc();
uint8_t r3 = sio_getc();
if (r0 == 'N' && r1 == 'O' && r2 == 'P' && r3 == 'E') {
return -1;
}
// Send raw data
const uint8_t *src = (const uint8_t *)buf;
for (int i = 0; i < len; i++) {
sio_putc(src[i]);
}
// Host responds: "OKAY" + bytesWritten(4)
if (!sio_check_okay()) return -1;
int written = (int)sio_read32();
return written;
}
/**
* PClseek — seek within a file
* Returns new position, or -1 on failure.
*/
static inline int PClseek(int fd, int offset, int whence) {
sio_pcdrv_escape(0x107);
// Host responds: "OKAY" (ready for params)
if (!sio_check_okay()) return -1;
// Send handle + offset + whence (seek origin)
sio_write32((uint32_t)fd);
sio_write32((uint32_t)offset);
sio_write32((uint32_t)whence);
// Host responds: "OKAY" + position(4) or "NOPE"
uint8_t r0 = sio_getc();
uint8_t r1 = sio_getc();
uint8_t r2 = sio_getc();
uint8_t r3 = sio_getc();
if (r0 == 'N' && r1 == 'O' && r2 == 'P' && r3 == 'E') {
return -1;
}
// "OKAY" — read new position
int pos = (int)sio_read32();
return pos;
}

View File

@@ -9,69 +9,66 @@
#include "bvh.hh" #include "bvh.hh"
#include "collision.hh" #include "collision.hh"
#include "gameobject.hh" #include "gameobject.hh"
#include "cutscene.hh"
#include "lua.h" #include "lua.h"
#include "mesh.hh" #include "mesh.hh"
#include "worldcollision.hh" #include "streq.hh"
#include "navregion.hh" #include "navregion.hh"
#include "renderer.hh" #include "renderer.hh"
namespace psxsplash { namespace psxsplash {
struct SPLASHPACKFileHeader { struct SPLASHPACKFileHeader {
char magic[2]; // "SP" char magic[2];
uint16_t version; // Format version (8 = movement params) uint16_t version;
uint16_t luaFileCount; uint16_t luaFileCount;
uint16_t gameObjectCount; uint16_t gameObjectCount;
uint16_t navmeshCount;
uint16_t textureAtlasCount; uint16_t textureAtlasCount;
uint16_t clutCount; uint16_t clutCount;
uint16_t colliderCount; uint16_t colliderCount;
uint16_t interactableCount;
psyqo::GTE::PackedVec3 playerStartPos; psyqo::GTE::PackedVec3 playerStartPos;
psyqo::GTE::PackedVec3 playerStartRot; psyqo::GTE::PackedVec3 playerStartRot;
psyqo::FixedPoint<12, uint16_t> playerHeight; psyqo::FixedPoint<12, uint16_t> playerHeight;
uint16_t sceneLuaFileIndex; uint16_t sceneLuaFileIndex;
// Version 3 additions:
uint16_t bvhNodeCount; uint16_t bvhNodeCount;
uint16_t bvhTriangleRefCount; uint16_t bvhTriangleRefCount;
// Version 4 additions (component counts): uint16_t sceneType;
uint16_t interactableCount; uint16_t triggerBoxCount;
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 worldCollisionMeshCount;
uint16_t worldCollisionTriCount; uint16_t worldCollisionTriCount;
uint16_t navRegionCount; uint16_t navRegionCount;
uint16_t navPortalCount; uint16_t navPortalCount;
// Version 8 additions (movement parameters): uint16_t moveSpeed;
uint16_t moveSpeed; // fp12 per-frame speed constant uint16_t sprintSpeed;
uint16_t sprintSpeed; // fp12 per-frame speed constant uint16_t jumpVelocity;
uint16_t jumpVelocity; // fp12 per-second initial jump velocity uint16_t gravity;
uint16_t gravity; // fp12 per-second² downward acceleration uint16_t playerRadius;
uint16_t playerRadius; // fp12 collision radius uint16_t pad1;
uint16_t reserved3; // Alignment padding uint32_t nameTableOffset;
// Version 9 additions (object names): uint16_t audioClipCount;
uint32_t nameTableOffset; // Offset to name string table (0 = no names) uint16_t pad2;
// Version 10 additions (audio): uint32_t audioTableOffset;
uint16_t audioClipCount; // Number of audio clips uint8_t fogEnabled;
uint16_t reserved4; // Alignment padding uint8_t fogR, fogG, fogB;
uint32_t audioTableOffset; // Offset to audio clip table (0 = no audio) uint8_t fogDensity;
// Version 11 additions (fog + room/portal): uint8_t pad3;
uint8_t fogEnabled; // 0 = off, 1 = on uint16_t roomCount;
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 portalCount;
uint16_t roomTriRefCount; 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) == 96, "SPLASHPACKFileHeader must be 96 bytes"); static_assert(sizeof(SPLASHPACKFileHeader) == 112, "SPLASHPACKFileHeader must be 112 bytes");
struct SPLASHPACKTextureAtlas { struct SPLASHPACKTextureAtlas {
uint32_t polygonsOffset; uint32_t polygonsOffset;
@@ -91,13 +88,12 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null"); psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null");
psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast<psxsplash::SPLASHPACKFileHeader *>(data); psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast<psxsplash::SPLASHPACKFileHeader *>(data);
psyqo::Kernel::assert(__builtin_memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic"); psyqo::Kernel::assert(__builtin_memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic");
psyqo::Kernel::assert(header->version >= 11, "Splashpack version too old: re-export from SplashEdit"); psyqo::Kernel::assert(header->version >= 17, "Splashpack version too old (need v17+): re-export from SplashEdit");
setup.playerStartPosition = header->playerStartPos; setup.playerStartPosition = header->playerStartPos;
setup.playerStartRotation = header->playerStartRot; setup.playerStartRotation = header->playerStartRot;
setup.playerHeight = header->playerHeight; setup.playerHeight = header->playerHeight;
// Movement parameters (v8+)
setup.moveSpeed.value = header->moveSpeed; setup.moveSpeed.value = header->moveSpeed;
setup.sprintSpeed.value = header->sprintSpeed; setup.sprintSpeed.value = header->sprintSpeed;
setup.jumpVelocity.value = header->jumpVelocity; setup.jumpVelocity.value = header->jumpVelocity;
@@ -107,13 +103,8 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
setup.luaFiles.reserve(header->luaFileCount); setup.luaFiles.reserve(header->luaFileCount);
setup.objects.reserve(header->gameObjectCount); setup.objects.reserve(header->gameObjectCount);
setup.colliders.reserve(header->colliderCount); setup.colliders.reserve(header->colliderCount);
setup.interactables.reserve(header->interactableCount);
// Reserve component arrays (version 4+)
if (header->version >= 4) {
setup.interactables.reserve(header->interactableCount);
}
// V11 header is always 96 bytes (validated by static_assert above).
uint8_t *cursor = data + sizeof(SPLASHPACKFileHeader); uint8_t *cursor = data + sizeof(SPLASHPACKFileHeader);
for (uint16_t i = 0; i < header->luaFileCount; i++) { for (uint16_t i = 0; i < header->luaFileCount; i++) {
@@ -123,7 +114,6 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += sizeof(psxsplash::LuaFile); 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; setup.sceneLuaFileIndex = (header->sceneLuaFileIndex == 0xFFFF) ? -1 : (int)header->sceneLuaFileIndex;
for (uint16_t i = 0; i < header->gameObjectCount; i++) { for (uint16_t i = 0; i < header->gameObjectCount; i++) {
@@ -133,15 +123,20 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += sizeof(psxsplash::GameObject); cursor += sizeof(psxsplash::GameObject);
} }
// Read collision data (after GameObjects)
for (uint16_t i = 0; i < header->colliderCount; i++) { for (uint16_t i = 0; i < header->colliderCount; i++) {
psxsplash::SPLASHPACKCollider *collider = reinterpret_cast<psxsplash::SPLASHPACKCollider *>(cursor); psxsplash::SPLASHPACKCollider *collider = reinterpret_cast<psxsplash::SPLASHPACKCollider *>(cursor);
setup.colliders.push_back(collider); setup.colliders.push_back(collider);
cursor += sizeof(psxsplash::SPLASHPACKCollider); cursor += sizeof(psxsplash::SPLASHPACKCollider);
} }
// Read BVH data (version 3+) setup.triggerBoxes.reserve(header->triggerBoxCount);
if (header->version >= 3 && header->bvhNodeCount > 0) { 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); BVHNode* bvhNodes = reinterpret_cast<BVHNode*>(cursor);
cursor += header->bvhNodeCount * sizeof(BVHNode); cursor += header->bvhNodeCount * sizeof(BVHNode);
@@ -151,58 +146,36 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
setup.bvh.initialize(bvhNodes, header->bvhNodeCount, setup.bvh.initialize(bvhNodes, header->bvhNodeCount,
triangleRefs, header->bvhTriangleRefCount); triangleRefs, header->bvhTriangleRefCount);
} }
// Read component data (version 4+) for (uint16_t i = 0; i < header->interactableCount; i++) {
if (header->version >= 4) { psxsplash::Interactable *interactable = reinterpret_cast<psxsplash::Interactable *>(cursor);
// Interactables setup.interactables.push_back(interactable);
for (uint16_t i = 0; i < header->interactableCount; i++) { cursor += sizeof(psxsplash::Interactable);
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+) // Skip over legacy world collision data if present in older binaries
if (header->version >= 7 && header->worldCollisionMeshCount > 0) { if (header->worldCollisionMeshCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor); uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3); cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.worldCollision.initializeFromData(cursor)); // 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)
} }
// Read nav regions (version 7+) if (header->navRegionCount > 0) {
if (header->version >= 7 && header->navRegionCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor); uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3); cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.navRegions.initializeFromData(cursor)); cursor = const_cast<uint8_t*>(setup.navRegions.initializeFromData(cursor));
} }
// Read room/portal data (version 11+, interior scenes) if (header->roomCount > 0) {
// 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); uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3); cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
@@ -219,42 +192,57 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += header->roomTriRefCount * sizeof(TriangleRef); 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++) { for (uint16_t i = 0; i < header->textureAtlasCount; i++) {
psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(cursor); psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(cursor);
uint8_t *offsetData = data + atlas->polygonsOffset; uint8_t *offsetData = data + atlas->polygonsOffset;
uint16_t *castedData = reinterpret_cast<uint16_t *>(offsetData); // Ensure 4-byte alignment for DMA transfer. If the exporter
psxsplash::Renderer::GetInstance().VramUpload(castedData, atlas->x, atlas->y, atlas->width, atlas->height); // 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); cursor += sizeof(psxsplash::SPLASHPACKTextureAtlas);
} }
for (uint16_t i = 0; i < header->clutCount; i++) { for (uint16_t i = 0; i < header->clutCount; i++) {
psxsplash::SPLASHPACKClut *clut = reinterpret_cast<psxsplash::SPLASHPACKClut *>(cursor); psxsplash::SPLASHPACKClut *clut = reinterpret_cast<psxsplash::SPLASHPACKClut *>(cursor);
uint8_t *clutOffset = data + clut->clutOffset; uint8_t *clutOffset = data + clut->clutOffset;
psxsplash::Renderer::GetInstance().VramUpload((uint16_t *)clutOffset, clut->clutPackingX * 16, // Same alignment guard for CLUT data.
clut->clutPackingY, clut->length, 1); 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); cursor += sizeof(psxsplash::SPLASHPACKClut);
} }
// Read object name table (version 9+) if (header->nameTableOffset != 0) {
if (header->version >= 9 && header->nameTableOffset != 0) {
uint8_t* nameData = data + header->nameTableOffset; uint8_t* nameData = data + header->nameTableOffset;
setup.objectNames.reserve(header->gameObjectCount); setup.objectNames.reserve(header->gameObjectCount);
for (uint16_t i = 0; i < header->gameObjectCount; i++) { for (uint16_t i = 0; i < header->gameObjectCount; i++) {
uint8_t nameLen = *nameData++; uint8_t nameLen = *nameData++;
const char* nameStr = reinterpret_cast<const char*>(nameData); const char* nameStr = reinterpret_cast<const char*>(nameData);
// Names are stored as length-prefixed, null-terminated strings
setup.objectNames.push_back(nameStr); setup.objectNames.push_back(nameStr);
nameData += nameLen + 1; // +1 for null terminator nameData += nameLen + 1; // +1 for null terminator
} }
} }
// Read audio clip table (version 10+) if (header->audioClipCount > 0 && header->audioTableOffset != 0) {
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; uint8_t* audioTable = data + header->audioTableOffset;
setup.audioClips.reserve(header->audioClipCount); setup.audioClips.reserve(header->audioClipCount);
setup.audioClipNames.reserve(header->audioClipCount); setup.audioClipNames.reserve(header->audioClipCount);
@@ -276,18 +264,189 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
} }
} }
// Read fog configuration (version 11+) setup.fogEnabled = header->fogEnabled != 0;
if (header->version >= 11) { setup.fogR = header->fogR;
setup.fogEnabled = header->fogEnabled != 0; setup.fogG = header->fogG;
setup.fogR = header->fogR; setup.fogB = header->fogB;
setup.fogG = header->fogG; setup.fogDensity = header->fogDensity;
setup.fogB = header->fogB; setup.sceneType = header->sceneType;
setup.fogDensity = header->fogDensity;
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++;
}
} }
// Read scene type (version 6+ stored it but it was never read until now) if (header->version >= 13) {
setup.sceneType = header->sceneType; 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

View File

@@ -8,39 +8,49 @@
#include "collision.hh" #include "collision.hh"
#include "gameobject.hh" #include "gameobject.hh"
#include "lua.h" #include "lua.h"
#include "worldcollision.hh"
#include "navregion.hh" #include "navregion.hh"
#include "audiomanager.hh" #include "audiomanager.hh"
#include "interactable.hh" #include "interactable.hh"
#include "cutscene.hh"
#include "animation.hh"
#include "uisystem.hh"
namespace psxsplash { namespace psxsplash {
/** /**
* Collision data as stored in the binary file (fixed layout for serialization) * Collision data as stored in the binary file (fixed layout for serialization)
* This is the binary-compatible version of CollisionData
*/ */
struct SPLASHPACKCollider { struct SPLASHPACKCollider {
// AABB bounds in fixed-point (24 bytes) // AABB bounds in fixed-point (24 bytes)
int32_t minX, minY, minZ; int32_t minX, minY, minZ;
int32_t maxX, maxY, maxZ; int32_t maxX, maxY, maxZ;
// Collision metadata (8 bytes) // Collision metadata (8 bytes)
uint8_t collisionType; // CollisionType enum uint8_t collisionType;
uint8_t layerMask; // Which layers this collides with uint8_t layerMask;
uint16_t gameObjectIndex; // Which GameObject this belongs to uint16_t gameObjectIndex;
uint32_t padding; // Alignment padding uint32_t padding;
}; };
static_assert(sizeof(SPLASHPACKCollider) == 32, "SPLASHPACKCollider must be 32 bytes"); 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 { struct SplashpackSceneSetup {
int sceneLuaFileIndex; int sceneLuaFileIndex;
eastl::vector<LuaFile *> luaFiles; eastl::vector<LuaFile *> luaFiles;
eastl::vector<GameObject *> objects; eastl::vector<GameObject *> objects;
eastl::vector<SPLASHPACKCollider *> colliders; eastl::vector<SPLASHPACKCollider *> colliders;
eastl::vector<SPLASHPACKTriggerBox *> triggerBoxes;
// New component arrays // New component arrays
eastl::vector<Interactable *> interactables; eastl::vector<Interactable *> interactables;
// Object name table (v9+): parallel to objects, points into splashpack data
eastl::vector<const char *> objectNames; eastl::vector<const char *> objectNames;
// Audio clips (v10+): ADPCM data with metadata // Audio clips (v10+): ADPCM data with metadata
@@ -53,12 +63,10 @@ struct SplashpackSceneSetup {
}; };
eastl::vector<AudioClipSetup> audioClips; eastl::vector<AudioClipSetup> audioClips;
// Audio clip name table (v10+): parallel to audioClips, points into splashpack data
eastl::vector<const char*> audioClipNames; eastl::vector<const char*> audioClipNames;
BVHManager bvh; // Spatial acceleration structure for culling BVHManager bvh; // Spatial acceleration structure for culling
WorldCollision worldCollision; // Triangle-level world collision (v7+) NavRegionSystem navRegions;
NavRegionSystem navRegions; // Convex region navigation (v7+)
psyqo::GTE::PackedVec3 playerStartPosition; psyqo::GTE::PackedVec3 playerStartPosition;
psyqo::GTE::PackedVec3 playerStartRotation; psyqo::GTE::PackedVec3 playerStartRotation;
psyqo::FixedPoint<12, uint16_t> playerHeight; psyqo::FixedPoint<12, uint16_t> playerHeight;
@@ -71,7 +79,6 @@ struct SplashpackSceneSetup {
uint8_t fogR = 0, fogG = 0, fogB = 0; uint8_t fogR = 0, fogG = 0, fogB = 0;
uint8_t fogDensity = 5; uint8_t fogDensity = 5;
// Room/portal data (v11+, interior scenes only)
const RoomData* rooms = nullptr; const RoomData* rooms = nullptr;
uint16_t roomCount = 0; uint16_t roomCount = 0;
const PortalData* portals = nullptr; const PortalData* portals = nullptr;
@@ -79,12 +86,24 @@ struct SplashpackSceneSetup {
const TriangleRef* roomTriRefs = nullptr; const TriangleRef* roomTriRefs = nullptr;
uint16_t roomTriRefCount = 0; uint16_t roomTriRefCount = 0;
// Movement parameters (v8+)
psyqo::FixedPoint<12, uint16_t> moveSpeed; // Per-frame speed constant (fp12) 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> sprintSpeed; // Per-frame sprint constant (fp12)
psyqo::FixedPoint<12, uint16_t> jumpVelocity; // Per-second initial velocity (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> gravity; // Per-second² acceleration (fp12)
psyqo::FixedPoint<12, uint16_t> playerRadius; // Collision radius (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;
}; };
class SplashPackLoader { class SplashPackLoader {
@@ -92,4 +111,4 @@ class SplashPackLoader {
void LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup); void LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup);
}; };
}; // namespace psxsplash } // 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

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

View File

@@ -1,4 +1,5 @@
// Auto-generated by SplashEdit - do not edit manually. // LEGACY -> used by loading screen
#pragma once #pragma once
// GPU resolution // GPU resolution

View File

@@ -3,16 +3,6 @@
#include <psyqo/fixed-point.hh> #include <psyqo/fixed-point.hh>
#include <psyqo/vector.hh> #include <psyqo/vector.hh>
// One-shot collision diagnostics
/**
* worldcollision.cpp - Player-vs-World Triangle Collision
*
* ALL math is 20.12 fixed-point. Intermediate products use int64_t
* to avoid overflow (20.12 * 20.12 = 40.24, shift >>12 back to 20.12).
*
* Performance budget: ~256 triangle tests per frame on 33MHz MIPS.
*/
namespace psxsplash { namespace psxsplash {
@@ -438,9 +428,6 @@ psyqo::Vec3 WorldCollision::moveAndSlide(const psyqo::Vec3& oldPos,
const auto& tri = m_triangles[mesh.firstTriangle + ti]; const auto& tri = m_triangles[mesh.firstTriangle + ti];
triTests++; triTests++;
// Skip trigger surfaces
if (tri.flags & SURFACE_TRIGGER) continue;
// Skip floor and ceiling triangles — Y is resolved by nav regions. // Skip floor and ceiling triangles — Y is resolved by nav regions.
// In PS1 space (Y-down): floor normals have ny < 0, ceiling ny > 0. // 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. // If |ny| > walkable slope threshold, it's a floor/ceiling, not a wall.
@@ -511,7 +498,6 @@ bool WorldCollision::groundTrace(const psyqo::Vec3& pos,
for (int ti = 0; ti < mesh.triangleCount; ti++) { for (int ti = 0; ti < mesh.triangleCount; ti++) {
const auto& tri = m_triangles[mesh.firstTriangle + ti]; const auto& tri = m_triangles[mesh.firstTriangle + ti];
if (tri.flags & SURFACE_TRIGGER) continue;
int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri); int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri);
if (t >= 0 && t < bestDist) { if (t >= 0 && t < bestDist) {
@@ -560,7 +546,6 @@ bool WorldCollision::ceilingTrace(const psyqo::Vec3& pos,
for (int ti = 0; ti < mesh.triangleCount; ti++) { for (int ti = 0; ti < mesh.triangleCount; ti++) {
const auto& tri = m_triangles[mesh.firstTriangle + ti]; const auto& tri = m_triangles[mesh.firstTriangle + ti];
if (tri.flags & SURFACE_TRIGGER) continue;
int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri); int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri);
if (t >= 0 && t < bestDist) { if (t >= 0 && t < bestDist) {

View File

@@ -22,11 +22,10 @@ namespace psxsplash {
// Surface flags — packed per-triangle, exported from SplashEdit // Surface flags — packed per-triangle, exported from SplashEdit
// ============================================================================ // ============================================================================
enum SurfaceFlag : uint8_t { enum SurfaceFlag : uint8_t {
SURFACE_SOLID = 0x01, // Normal solid wall/floor SURFACE_SOLID = 0x01,
SURFACE_SLOPE = 0x02, // Steep slope (treated as wall for movement) SURFACE_SLOPE = 0x02,
SURFACE_STAIRS = 0x04, // Staircase (smooth Y interpolation) SURFACE_STAIRS = 0x04,
SURFACE_TRIGGER = 0x08, // Non-solid trigger volume SURFACE_NO_WALK = 0x10,
SURFACE_NO_WALK = 0x10, // Marks geometry as non-walkable floor
}; };
// ============================================================================ // ============================================================================
@@ -142,6 +141,12 @@ public:
/// Is collision data loaded? /// Is collision data loaded?
bool isLoaded() const { return m_triangles != nullptr; } 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 // High-level queries used by the player movement system
// ======================================================================== // ========================================================================

Binary file not shown.

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;
}