This commit is contained in:
Jan Racek
2026-03-27 21:29:01 +01:00
parent 85eb7e59d9
commit bfab154547
42 changed files with 118 additions and 1013 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,7 +13,6 @@ src/triclip.cpp \
src/lua.cpp \
src/luaapi.cpp \
src/scenemanager.cpp \
src/sceneloader.cpp \
src/fileloader.cpp \
src/audiomanager.cpp \
src/controls.cpp \

Binary file not shown.

Binary file not shown.

View File

@@ -81,32 +81,32 @@ int BVHManager::traverseRegion(int nodeIndex, int32_t qMinX, int32_t qMinY,
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;
}
if (node.isLeaf()) {
int count = node.triangleCount;
int available = maxRefs - currentCount;
if (count > available)
count = available;
for (int i = 0; i < count; i++) {
outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i];
}
return currentCount + count;
}
if (node.leftChild != 0xFFFF) {
currentCount =
traverseRegion(node.leftChild, qMinX, qMinY, qMinZ, qMaxX, qMaxY,
qMaxZ, outRefs, currentCount, maxRefs);
}
if (node.rightChild != 0xFFFF) {
currentCount =
traverseRegion(node.rightChild, qMinX, qMinY, qMinZ, qMaxX, qMaxY,
qMaxZ, outRefs, currentCount, maxRefs);
}
return currentCount;
}
bool BVHManager::aabbOverlap(const BVHNode &node, int32_t qMinX, int32_t qMinY,

View File

@@ -28,12 +28,6 @@ struct BVHNode {
return leftChild == 0xFFFF && rightChild == 0xFFFF;
}
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;
}
bool testPlane(int32_t nx, int32_t ny, int32_t nz, int32_t d) const {
int32_t px = (nx >= 0) ? maxX : minX;
int32_t py = (ny >= 0) ? maxY : minY;

View File

@@ -14,7 +14,7 @@ class Camera {
void MoveX(psyqo::FixedPoint<12> x);
void MoveY(psyqo::FixedPoint<12> y);
void MoveZ(psyqo::FixedPoint<12> y);
void MoveZ(psyqo::FixedPoint<12> z);
void SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z);
psyqo::Vec3& GetPosition() { return m_position; }

View File

@@ -55,12 +55,6 @@ void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz)
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) {
int minGx, minGy, minGz;
int maxGx, maxGy, maxGz;
@@ -308,98 +302,4 @@ bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
return true;
}
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 {
auto closestT = maxDistance;
bool hit = false;
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;
for (int i = 0; i < m_colliderCount; i++) {
const CollisionData& collider = m_colliders[i];
if (collider.type == CollisionType::None) continue;
const AABB& box = collider.bounds;
auto tMin = negLargeVal;
auto tMax = largeVal;
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;
}
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;
}
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;
hitPoint = psyqo::Vec3{
origin.x + direction.x * t,
origin.y + direction.y * t,
origin.z + direction.z * t
};
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;
}
} // namespace psxsplash

View File

@@ -33,22 +33,6 @@ struct AABB {
(point.z >= min.z && point.z <= max.z);
}
psyqo::Vec3 center() const {
return psyqo::Vec3{
(min.x + max.x) / 2,
(min.y + max.y) / 2,
(min.z + max.z) / 2
};
}
psyqo::Vec3 halfExtents() const {
return psyqo::Vec3{
(max.x - min.x) / 2,
(max.y - min.y) / 2,
(max.z - min.z) / 2
};
}
void expand(const psyqo::Vec3& delta);
};
static_assert(sizeof(AABB) == 24);
@@ -101,7 +85,6 @@ public:
void clear();
void insert(uint16_t objectIndex, const AABB& bounds);
int queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const;
int getCellIndex(const psyqo::Vec3& pos) const;
private:
Cell m_cells[CELL_COUNT];
@@ -134,13 +117,6 @@ public:
const CollisionResult* getResults() const { return m_results; }
int getResultCount() const { return m_resultCount; }
bool areColliding(uint16_t indexA, uint16_t indexB) const;
bool raycast(const psyqo::Vec3& origin, const psyqo::Vec3& direction,
psyqo::FixedPoint<12> maxDistance,
psyqo::Vec3& hitPoint, psyqo::Vec3& hitNormal,
uint16_t& hitObjectIndex) const;
int getColliderCount() const { return m_colliderCount; }
private:

View File

@@ -12,7 +12,7 @@ using namespace psyqo::trig_literals;
class Controls {
public:
/// Force DualShock into analog mode (LED=Red) via raw SIO commands.
/// Force DualShock into analog mode
/// Must be called BEFORE Init() since Init() hands SIO control to AdvancedPad.
void forceAnalogMode();
@@ -87,4 +87,4 @@ class Controls {
void getDpadAxes(int16_t &outX, int16_t &outY) const;
};
}; // namespace psxsplash
} // namespace psxsplash

View File

@@ -3,18 +3,11 @@
#include <psyqo/fixed-point.hh>
#include <psyqo/soft-math.hh>
#include <psyqo/trigonometry.hh>
#include "streq.hh"
#include "uisystem.hh"
namespace psxsplash {
// Bare-metal string compare (avoids linking libc)
static bool cs_streq(const char* a, const char* b) {
while (*a && *b) {
if (*a++ != *b++) return false;
}
return *a == *b;
}
void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio,
UISystem* uiSystem) {
m_cutscenes = cutscenes;
@@ -31,7 +24,7 @@ bool CutscenePlayer::play(const char* name) {
if (!name || !m_cutscenes) return false;
for (int i = 0; i < m_count; i++) {
if (m_cutscenes[i].name && cs_streq(m_cutscenes[i].name, name)) {
if (m_cutscenes[i].name && streq(m_cutscenes[i].name, name)) {
m_active = &m_cutscenes[i];
m_frame = 0;
m_nextAudio = 0;
@@ -64,7 +57,7 @@ bool CutscenePlayer::play(const char* name) {
}
break;
case TrackType::ObjectRotationY:
// Can't easily recover angle from matrix — default to 0
// TODO: I added this. Then realized that it only stores rotation matrix. That sucks. To anyone who finds this Pull requests are open :P
break;
case TrackType::ObjectActive:
if (track.target) {
@@ -119,12 +112,10 @@ void CutscenePlayer::stop() {
void CutscenePlayer::tick() {
if (!m_active) return;
// Apply all tracks at the current frame
for (uint8_t i = 0; i < m_active->trackCount; i++) {
applyTrack(m_active->tracks[i]);
}
// Fire audio events whose frame has been reached
while (m_nextAudio < m_active->audioEventCount) {
CutsceneAudioEvent& evt = m_active->audioEvents[m_nextAudio];
if (evt.frame <= m_frame) {
@@ -137,39 +128,31 @@ void CutscenePlayer::tick() {
}
}
// Advance frame
m_frame++;
if (m_frame > m_active->totalFrames) {
m_active = nullptr; // Cutscene finished
}
}
// Apply easing curve to fixed-point t ∈ [0, 4096].
static int32_t applyCurve(int32_t t, InterpMode mode) {
switch (mode) {
default:
case InterpMode::Linear:
return t;
case InterpMode::Step:
return 0; // snaps to 'a' value until next keyframe
return 0;
case InterpMode::EaseIn:
// t² / 4096
return (int32_t)((int64_t)t * t >> 12);
case InterpMode::EaseOut:
// 1 (1t)² = t*(2 t) / 4096
return (int32_t)(((int64_t)t * (8192 - t)) >> 12);
case InterpMode::EaseInOut: {
// Smoothstep: 3t² 2t³ (all in 12-bit fixed point)
int64_t t2 = (int64_t)t * t; // 24-bit
int64_t t3 = t2 * t; // 36-bit
int64_t t2 = (int64_t)t * t;
int64_t t3 = t2 * t;
return (int32_t)((3 * t2 - 2 * (t3 >> 12)) >> 12);
}
}
}
// Common helper: find surrounding keyframes and compute fixed-point t (0..4096).
// Returns false if result was clamped (no interpolation needed, out[] already set).
// When true, t already has the easing curve applied based on the *destination* keyframe's InterpMode.
static 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) {
@@ -208,7 +191,6 @@ void CutscenePlayer::lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const in
uint8_t a, b;
int32_t t;
if (!findKfPair(kf, count, m_frame, a, b, t, out)) {
// If clamped to first keyframe and frame < first kf frame, blend from initial
if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) {
uint16_t span = kf[0].getFrame();
uint32_t num = (uint32_t)m_frame << 12;
@@ -228,8 +210,6 @@ void CutscenePlayer::lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const in
}
}
// Shortest-path angle interpolation.
// Angles are in psyqo units where 2048 = full circle (2π).
static constexpr int32_t ANGLE_FULL_CIRCLE = 2048;
static constexpr int32_t ANGLE_HALF_CIRCLE = 1024;
@@ -237,7 +217,6 @@ void CutscenePlayer::lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16
uint8_t a, b;
int32_t t;
if (!findKfPair(kf, count, m_frame, a, b, t, out)) {
// If clamped to first keyframe and frame < first kf frame, blend from initial
if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) {
uint16_t span = kf[0].getFrame();
uint32_t num = (uint32_t)m_frame << 12;
@@ -257,9 +236,7 @@ void CutscenePlayer::lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16
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];
// Shortest-path: wrap delta into [-1024, +1024)
int32_t delta = to - from;
// Modulo into [-2048, +2048) then clamp to half-circle
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));
}
@@ -314,10 +291,8 @@ void CutscenePlayer::applyTrack(CutsceneTrack& track) {
case TrackType::ObjectActive: {
if (!track.target) return;
// Step interpolation: find the last keyframe at or before m_frame
CutsceneKeyframe* kf = track.keyframes;
uint8_t count = track.keyframeCount;
// Use initial state if we're before the first keyframe
int16_t activeVal = (count > 0 && m_frame < kf[0].getFrame())
? track.initialValues[0]
: kf[0].values[0];

View File

@@ -24,22 +24,19 @@ enum class TrackType : uint8_t {
ObjectPosition = 2,
ObjectRotationY = 3,
ObjectActive = 4,
// UI track types (v13+)
UICanvasVisible = 5, // Step: values[0] = 0/1. target unused, uiHandle = canvas index.
UIElementVisible= 6, // Step: values[0] = 0/1. uiHandle = element handle.
UIProgress = 7, // Linear: values[0] = 0-100. uiHandle = element handle.
UIPosition = 8, // Linear: values[0] = x, values[1] = y. uiHandle = element handle.
UIColor = 9, // Linear: values[0] = r, values[1] = g, values[2] = b. uiHandle = element handle.
UICanvasVisible = 5,
UIElementVisible= 6,
UIProgress = 7,
UIPosition = 8,
UIColor = 9,
};
/// Per-keyframe interpolation mode.
/// Packed into upper 3 bits of the frame field in CutsceneKeyframe.
enum class InterpMode : uint8_t {
Linear = 0, // Default linear interpolation
Step = 1, // Instant jump (no interpolation)
EaseIn = 2, // Slow start, fast end (quadratic)
EaseOut = 3, // Fast start, slow end (quadratic)
EaseInOut = 4, // Smooth start and end (smoothstep)
Linear = 0,
Step = 1,
EaseIn = 2,
EaseOut = 3,
EaseInOut = 4,
};
struct CutsceneKeyframe {
@@ -66,16 +63,9 @@ struct CutsceneTrack {
TrackType trackType;
uint8_t keyframeCount;
uint8_t pad[2];
CutsceneKeyframe* keyframes; // Points into splashpack data (resolved at load time)
GameObject* target; // nullptr = camera track or UI track
/// For UI tracks: flat handle into UISystem (canvas index or element handle).
/// Set during cutscene load by resolving canvas/element names.
CutsceneKeyframe* keyframes;
GameObject* target;
int16_t uiHandle;
/// Initial values captured at play() time for pre-first-keyframe blending.
/// For position tracks: fp12 x,y,z. For rotation tracks: raw angle values.
/// For ObjectActive: values[0] = 1 (active) or 0 (inactive).
int16_t initialValues[3];
};
@@ -88,8 +78,7 @@ struct Cutscene {
CutsceneAudioEvent* audioEvents; // Points into splashpack data
};
/// Zero-allocation cutscene player. Call init() once after splashpack is loaded,
/// then tick() once per frame from the scene loop.
class CutscenePlayer {
public:
/// Initialize with loaded cutscene data. Safe to pass nullptr/0 if no cutscenes.

View File

@@ -19,9 +19,8 @@ namespace psxsplash {
* 4. ISO9660Parser::scheduleGetDirentry — look up a file by path
* 5. ISO9660Parser::scheduleReadRequest — read the file sectors
*
* The BIOS has already read SYSTEM.CNF and launched the executable by the
* time we get here, so we don't touch SYSTEM.CNF at all.
*/
class FileLoaderCDRom final : public FileLoader {
public:
FileLoaderCDRom() : m_isoParser(&m_cdrom) {}
@@ -99,7 +98,7 @@ class FileLoaderCDRom final : public FileLoader {
// ── LoadFileSyncWithProgress ───────────────────────────────
// Reads the file in 32-sector (64 KB) chunks, calling the
// progress callback between each chunk so the loading bar
// animates during the (slow) CD-ROM transfer.
// animates during the CD-ROM transfer.
uint8_t* LoadFileSyncWithProgress(
const char* filename, int& outSize,
const LoadProgressInfo* progress) override

View File

@@ -12,12 +12,6 @@ namespace psxsplash {
* - 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)
*
* Both modes are preserved — we call pcdrv_sio1_init() first (installs
* the SIO1 break handler + redirects printf), then pcdrv_init() which
* checks for the PCSX-Redux emulator and falls back to SIO1 if absent.
*
* All tasks resolve synchronously since PCdrv I/O is blocking.
*/
class FileLoaderPCdrv final : public FileLoader {
public:
@@ -27,11 +21,9 @@ class FileLoaderPCdrv final : public FileLoader {
// ── scheduleInit ──────────────────────────────────────────────
psyqo::TaskQueue::Task scheduleInit() override {
return psyqo::TaskQueue::Task([this](psyqo::TaskQueue::Task* task) {
// Set up SIO1 break handler + printf redirect (real-hardware path).
// On emulator this is harmless — pcsx-redux ignores SIO1 setup.
pcdrv_sio1_init();
// Detect emulator vs SIO1 and initialise the active transport.
m_available = (pcdrv_init() == 0);
task->resolve();
});

View File

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

View File

@@ -1,7 +1,6 @@
#include "loadingscreen.hh"
#include "fileloader.hh"
#include <psyqo/kernel.hh>
extern "C" int ramsyscall_printf(const char*, ...);
#include "renderer.hh"
#include <psyqo/primitives/rectangles.hh>
@@ -12,23 +11,9 @@ extern "C" int ramsyscall_printf(const char*, ...);
namespace psxsplash {
// psyqo places the second framebuffer at VRAM Y=256 regardless of the
// actual display height (240 for NTSC, 256 for PAL). All double-buffer
// rendering must use this constant, NOT m_resH, to match the display
// registers set by GPU::flip().
static constexpr int16_t kBuffer1Y = 256;
// ────────────────────────────────────────────────────────────────
// Bare-metal string helpers (no libc)
// ────────────────────────────────────────────────────────────────
static void int_to_str(int val, char* buf) {
if (val < 0) { *buf++ = '-'; val = -val; }
char tmp[12];
int len = 0;
do { tmp[len++] = '0' + (val % 10); val /= 10; } while (val > 0);
for (int i = len - 1; i >= 0; i--) *buf++ = tmp[i];
*buf = '\0';
}
// This file has duplicate code from UISystem... This is terrible...
// ────────────────────────────────────────────────────────────────
// Load
@@ -37,22 +22,15 @@ bool LoadingScreen::load(psyqo::GPU& gpu, psyqo::Font<>& systemFont, int sceneIn
// Build filename using the active backend's naming convention
char filename[32];
FileLoader::BuildLoadingFilename(sceneIndex, filename, sizeof(filename));
ramsyscall_printf("LoadingScreen: loading '%s'\n", filename);
int fileSize = 0;
uint8_t* data = FileLoader::Get().LoadFileSync(filename, fileSize);
if (!data || fileSize < (int)sizeof(LoaderPackHeader)) {
ramsyscall_printf("LoadingScreen: FAILED to load (data=%p size=%d)\n", data, fileSize);
if (data) FileLoader::Get().FreeFile(data);
return false;
}
auto* header = reinterpret_cast<const LoaderPackHeader*>(data);
ramsyscall_printf("LoadingScreen: magic='%c%c' ver=%d canvases=%d fonts=%d atlases=%d cluts=%d tableOff=%u\n",
header->magic[0], header->magic[1], header->version,
header->canvasCount, header->fontCount, header->atlasCount, header->clutCount, header->tableOffset);
if (header->magic[0] != 'L' || header->magic[1] != 'P') {
ramsyscall_printf("LoadingScreen: bad magic, aborting\n");
FileLoader::Get().FreeFile(data);
return false;
}
@@ -73,17 +51,12 @@ bool LoadingScreen::load(psyqo::GPU& gpu, psyqo::Font<>& systemFont, int sceneIn
m_ui.uploadFonts(gpu);
// Ensure canvas 0 is visible
ramsyscall_printf("LoadingScreen: canvasCount=%d\n", m_ui.getCanvasCount());
if (m_ui.getCanvasCount() > 0) {
m_ui.setCanvasVisible(0, true);
int elemCount = m_ui.getCanvasElementCount(0);
ramsyscall_printf("LoadingScreen: canvas 0 has %d elements\n", elemCount);
}
// Find the progress bar named "loading"
findProgressBar();
ramsyscall_printf("LoadingScreen: hasProgressBar=%d barXY=(%d,%d) barWH=(%d,%d)\n",
m_hasProgressBar, m_barX, m_barY, m_barW, m_barH);
return true;
}
@@ -170,7 +143,7 @@ void LoadingScreen::drawImage(psyqo::GPU& gpu, int handle,
const UIImageData* img = m_ui.getImageData(handle);
if (!img) return;
// Build TPage attribute (same as UISystem::makeTPage)
// Build TPage attribute
psyqo::PrimPieces::TPageAttr tpage;
tpage.setPageX(img->texpageX);
tpage.setPageY(img->texpageY);
@@ -372,8 +345,6 @@ void LoadingScreen::renderInitialAndFree(psyqo::GPU& gpu) {
FileLoader::Get().FreeFile(m_data);
m_data = nullptr;
m_dataSize = 0;
// m_ui now points into freed memory — don't use it again.
// m_active remains true so updateProgress keeps working.
}
// ────────────────────────────────────────────────────────────────

View File

@@ -8,7 +8,6 @@
namespace psxsplash {
/// Loader pack header — matches the binary written by PSXLoaderPackWriter.cs.
struct LoaderPackHeader {
char magic[2]; // "LP"
uint16_t version; // 2
@@ -22,7 +21,6 @@ struct LoaderPackHeader {
};
static_assert(sizeof(LoaderPackHeader) == 16, "LoaderPackHeader must be 16 bytes");
/// Atlas entry in the loader pack (matches SPLASHPACKTextureAtlas layout).
struct LoaderPackAtlas {
uint32_t pixelDataOffset; // absolute offset in file
uint16_t width, height;
@@ -30,7 +28,6 @@ struct LoaderPackAtlas {
};
static_assert(sizeof(LoaderPackAtlas) == 12, "LoaderPackAtlas must be 12 bytes");
/// CLUT entry in the loader pack (matches SPLASHPACKClut layout).
struct LoaderPackClut {
uint32_t clutDataOffset; // absolute offset in file
uint16_t clutX; // VRAM X (in 16-pixel units × 16)
@@ -40,25 +37,7 @@ struct LoaderPackClut {
};
static_assert(sizeof(LoaderPackClut) == 12, "LoaderPackClut must be 12 bytes");
/// Loading screen controller.
///
/// Loads a .loading file (tiny UI-only binary), renders the screen,
/// and provides progress updates during the main splashpack load.
///
/// Strategy:
/// 1. Blank the screen immediately (user sees black, not frozen frame).
/// 2. Load the .loading file (small, fast).
/// 3. Upload texture atlases, CLUTs, and custom font textures to VRAM.
/// 4. Render ALL elements to BOTH framebuffers using direct GPU commands:
/// - Images via sendPrimitive(GouraudTexturedTriangle)
/// - Custom font text via sendPrimitive(TPage) + sendPrimitive(Sprite)
/// - System font text via font.print()
/// - Boxes and progress bars via sendPrimitive(Rectangle)
/// 5. FREE all loaded data (loader pack) so the splashpack has room.
/// 6. During splashpack loading, call updateProgress() to redraw ONLY the
/// progress bar (simple rectangles in both framebuffers — no VRAM needed).
///
/// The progress bar is found by looking for a PSXUIProgressBar element named "loading".
class LoadingScreen {
public:
/// Try to load a loader pack from a file.

View File

@@ -92,26 +92,23 @@ static int gameobjectSetPosition(psyqo::Lua L) {
}
static int gamobjectGetActive(psyqo::Lua L) {
static int gameobjectGetActive(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1);
L.push(go->isActive());
return 1;
}
static int gamobjectSetActive(psyqo::Lua L) {
static int gameobjectSetActive(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1);
bool active = L.toBoolean(2);
go->setActive(active);
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;
// 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) {
psyqo::Angle result;
if (cosVal == 0 && sinVal == 0) { result.value = 0; return result; }
@@ -121,17 +118,10 @@ static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) {
int32_t minV = 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;
// Past 45°: use complement
if (abs_s > abs_c) angle = 512 - angle; // π/2 - angle
// Quadrant 2/3: cos < 0
if (cosVal < 0) angle = 1024 - angle; // π - angle
// Quadrant 3/4: sin < 0
if (abs_s > abs_c) angle = 512 - angle;
if (cosVal < 0) angle = 1024 - angle;
if (sinVal < 0) angle = -angle;
result.value = angle;
@@ -140,11 +130,9 @@ static psyqo::Angle fastAtan2(int32_t sinVal, int32_t cosVal) {
static int gameobjectGetRotationY(psyqo::Lua L) {
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 cosRaw = go->rotation.vs[0].x.raw();
psyqo::Angle angle = fastAtan2(sinRaw, cosRaw);
// Return in pi-units: 0.5 = π/2 = 90°
L.pushNumber(static_cast<lua_Number>(angle.value) / kAngleScale);
return 1;
}
@@ -172,10 +160,10 @@ void psxsplash::Lua::Init() {
L.push(gameobjectSetPosition);
L.setField(-2, "set_position");
L.push(gamobjectGetActive);
L.push(gameobjectGetActive);
L.setField(-2, "get_active");
L.push(gamobjectSetActive);
L.push(gameobjectSetActive);
L.setField(-2, "set_active");
L.push(gameobjectGetRotationY);
@@ -212,8 +200,7 @@ void psxsplash::Lua::Init() {
}
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()) {
m_state.close();
}
@@ -223,9 +210,8 @@ void psxsplash::Lua::Shutdown() {
}
void psxsplash::Lua::Reset() {
// Nuclear reset: destroy the entire Lua VM and create a fresh one.
Shutdown();
m_state = psyqo::Lua(); // fresh state (luaL_newstate + openlibs)
m_state = psyqo::Lua();
Init();
}
@@ -298,8 +284,6 @@ void psxsplash::Lua::RegisterSceneScripts(int index) {
// 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) {
uint8_t* ptr = reinterpret_cast<uint8_t*>(go);
auto L = m_state;
@@ -431,7 +415,6 @@ void psxsplash::Lua::OnTriggerExitScript(int luaFileIndex, int triggerIndex) {
void psxsplash::Lua::OnDestroy(GameObject* go) {
if (!hasEvent(go, EVENT_ON_DESTROY)) return;
onDestroyMethodWrapper.callMethod(*this, go);
// Clear the event mask when object is destroyed
go->eventMask = EVENT_NONE;
}
@@ -463,8 +446,6 @@ void psxsplash::Lua::OnUpdate(GameObject* go, int 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++) {
// objects[i] is already the NEW pointer (relocated by shrinkBuffer).
// Compute the OLD pointer by subtracting delta.
uint8_t* newPtr = reinterpret_cast<uint8_t*>(objects[i]);
uint8_t* oldPtr = newPtr - delta;
@@ -507,7 +488,7 @@ void psxsplash::Lua::PushGameObject(GameObject* go) {
L.rawGet(LUA_REGISTRYINDEX);
if (!L.isTable(-1)) {
printf("Warning: GameObject not found in Lua registry\n");
L.pop();
L.push(); // push nil so the caller always gets a value
}
}

View File

@@ -158,7 +158,7 @@ class Lua {
[[no_unique_address]] FunctionWrapper<1, typestring_is("onSceneCreationStart")> onSceneCreationStartFunctionWrapper;
[[no_unique_address]] FunctionWrapper<2, typestring_is("onSceneCreationEnd")> onSceneCreationEndFunctionWrapper;
// Object-level events (methodId 100+, offset to avoid collision with scene events)
// Object-level events
[[no_unique_address]] FunctionWrapper<100, typestring_is("onCreate")> onCreateMethodWrapper;
[[no_unique_address]] FunctionWrapper<101, typestring_is("onCollideWithPlayer")> onCollideWithPlayerMethodWrapper;
[[no_unique_address]] FunctionWrapper<102, typestring_is("onInteract")> onInteractMethodWrapper;

View File

@@ -9,7 +9,7 @@
#include <psyqo/soft-math.hh>
#include <psyqo/trigonometry.hh>
#include <psyqo/fixed-point.hh>
#include <psyqo/xprintf.h>
namespace psxsplash {
@@ -241,7 +241,7 @@ void LuaAPI::RegisterAll(psyqo::Lua& L, SceneManager* scene, CutscenePlayer* cut
L.push(Math_Max);
L.setField(-2, "Max");
L.setGlobal("Math");
L.setGlobal("PSXMath");
// ========================================================================
// SCENE API
@@ -615,7 +615,8 @@ int LuaAPI::Entity_ForEach(lua_State* L) {
lua.pop(2); // pop non-table + callback copy
continue;
}
if (lua.pcall(1, 0) != LUA_OK) {
lua.pushNumber(i); // push index as second argument
if (lua.pcall(2, 0) != LUA_OK) {
lua.pop(); // pop error message
}
}
@@ -1179,28 +1180,27 @@ int LuaAPI::Camera_LookAt(lua_State* L) {
// ============================================================================
int LuaAPI::Audio_Play(lua_State* L) {
printf("[Audio_Play] ENTER top=%d\n", lua_gettop(L));
psyqo::Lua lua(L);
if (!s_sceneManager) {
lua.pushNumber(-1);
return 1;
}
int soundId = -1;
// Accept number (index) or string (name lookup) like Entity.Find
// Check isNumber FIRST in Lua, numbers pass isString too.
// Check isNumber FIRST - in Lua, numbers pass isString too.
if (lua.isNumber(1)) {
soundId = static_cast<int>(lua.toNumber(1));
printf("[Audio_Play] by index: %d\n", soundId);
} else if (lua.isString(1)) {
const char* name = lua.toString(1);
printf("[Audio_Play] by name: '%s'\n", name ? name : "(null)");
soundId = s_sceneManager->findAudioClipByName(name);
if (soundId < 0) {
printf("[Audio_Play] clip not found\n");
lua.pushNumber(-1);
return 1;
}
printf("[Audio_Play] found at index %d\n", soundId);
} else {
printf("[Audio_Play] invalid arg type\n");
lua.pushNumber(-1);
return 1;
}
@@ -1208,9 +1208,7 @@ int LuaAPI::Audio_Play(lua_State* L) {
int volume = static_cast<int>(lua.optNumber(2, 100));
int pan = static_cast<int>(lua.optNumber(3, 64));
printf("[Audio_Play] play(%d, vol=%d, pan=%d)\n", soundId, volume, pan);
int voice = s_sceneManager->getAudio().play(soundId, volume, pan);
printf("[Audio_Play] voice=%d OK\n", voice);
lua.pushNumber(voice);
return 1;
}
@@ -1236,6 +1234,7 @@ int LuaAPI::Audio_Find(lua_State* L) {
int LuaAPI::Audio_Stop(lua_State* L) {
psyqo::Lua lua(L);
if (!s_sceneManager) return 0;
int channelId = static_cast<int>(lua.toNumber(1));
s_sceneManager->getAudio().stopVoice(channelId);
return 0;
@@ -1243,6 +1242,7 @@ int LuaAPI::Audio_Stop(lua_State* L) {
int LuaAPI::Audio_SetVolume(lua_State* L) {
psyqo::Lua lua(L);
if (!s_sceneManager) return 0;
int channelId = static_cast<int>(lua.toNumber(1));
int volume = static_cast<int>(lua.toNumber(2));
int pan = static_cast<int>(lua.optNumber(3, 64));
@@ -1252,6 +1252,7 @@ int LuaAPI::Audio_SetVolume(lua_State* L) {
int LuaAPI::Audio_StopAll(lua_State* L) {
psyqo::Lua lua(L);
if (!s_sceneManager) return 0;
s_sceneManager->getAudio().stopAll();
return 0;
}
@@ -1263,9 +1264,8 @@ int LuaAPI::Audio_StopAll(lua_State* L) {
int LuaAPI::Debug_Log(lua_State* L) {
psyqo::Lua lua(L);
const char* msg = lua.optString(1, "");
printf("[Lua] %s\n", msg);
// Debug.Log is a no-op on PS1 to avoid printf overhead.
// Messages are only visible in emulator TTY builds.
return 0;
}
@@ -1280,12 +1280,6 @@ int LuaAPI::Debug_DrawLine(lua_State* L) {
}
// TODO: Queue LINE_G2 primitive through Renderer
// For now, log to debug console (visible in emulator)
#ifdef PSXSPLASH_DEBUG_DRAW
printf("[DebugDraw] Line (%d,%d,%d)->(%d,%d,%d)\n",
sx.raw() >> 12, sy.raw() >> 12, sz.raw() >> 12,
ex.raw() >> 12, ey.raw() >> 12, ez.raw() >> 12);
#endif
return 0;
}
@@ -1300,11 +1294,6 @@ int LuaAPI::Debug_DrawBox(lua_State* L) {
}
// TODO: Queue 12 LINE_G2 primitives (box wireframe) through Renderer
#ifdef PSXSPLASH_DEBUG_DRAW
printf("[DebugDraw] Box center(%d,%d,%d) size(%d,%d,%d)\n",
cx.raw() >> 12, cy.raw() >> 12, cz.raw() >> 12,
hx.raw() >> 12, hy.raw() >> 12, hz.raw() >> 12);
#endif
return 0;
}

View File

@@ -24,7 +24,6 @@ namespace psxsplash {
uint16_t padding;
/// 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 {
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);
}
// ============================================================================
// 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)
// ============================================================================
@@ -316,36 +282,10 @@ int32_t NavRegionSystem::resolvePosition(int32_t& newX, int32_t& newZ,
bool NavRegionSystem::findPath(uint16_t startRegion, uint16_t endRegion,
NavPath& path) const {
// 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;
(void)startRegion;
(void)endRegion;
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

View File

@@ -9,9 +9,6 @@
* - Player has a single current region index.
* - Movement: point-in-convex-polygon test → portal crossing → neighbor update.
* - Floor Y: project XZ onto region's floor plane.
* - Pathfinding: A* over region adjacency graph (stub ready for NPC drop-in).
*
* All math is fixed-point 20.12. Zero floats.
*/
#include <stdint.h>
@@ -148,24 +145,10 @@ public:
/// Returns the clamped position.
void clampToRegion(int32_t& x, int32_t& z, uint16_t regionIndex) const;
// ========================================================================
// 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.
// TODO: Implement this
bool findPath(uint16_t startRegion, uint16_t endRegion,
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:
NavDataHeader m_header = {};
const NavRegion* m_regions = nullptr;
@@ -183,11 +166,6 @@ private:
int32_t bx, int32_t bz,
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

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
#include <stdint.h>
#include <stdarg.h>
#include <psyqo/kernel.hh>
#include <psyqo/xprintf.h>
#include "common/hardware/pcsxhw.h"
#include "common/kernel/pcdrv.h"
@@ -79,18 +60,6 @@ static inline void sio_pcdrv_escape(uint32_t 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() {
sio_pcdrv_escape(0x101);
@@ -114,19 +83,6 @@ static int sio_pcdrv_open(const char* name, int flags) {
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) {
sio_pcdrv_escape(0x104);
if (!sio_check_okay()) return -1;
@@ -158,23 +114,6 @@ static int sio_pcdrv_read(int fd, void* buf, int len) {
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) {
sio_pcdrv_escape(0x107);
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);
}
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) {
if (pcsx_present()) return PCclose(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);
}
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) {
if (pcsx_present()) return PClseek(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
}
// =========================================================================
// 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() {
if (pcsx_present()) return; // emulator handles PCDRV natively

View File

@@ -57,12 +57,6 @@ void psxsplash::Renderer::SetFog(const FogConfig& fog) {
}
}
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() {
::clear<Register::TRX, Safe>();
::clear<Register::TRY, Safe>();
@@ -270,7 +264,7 @@ void psxsplash::Renderer::Render(eastl::vector<GameObject*>& objects) {
auto& ditherCmd = balloc.allocateFragment<psyqo::Prim::TPage>();
ditherCmd.primitive.attr.setDithering(true);
ot.insert(ditherCmd, ORDERING_TABLE_SIZE - 1);
writeFogRegisters();
psyqo::Vec3 cameraPosition = computeCameraViewPos();
int32_t fogFarSZ = m_fog.fogFarSZ;
for (auto& obj : objects) {
@@ -301,7 +295,7 @@ void psxsplash::Renderer::RenderWithBVH(eastl::vector<GameObject*>& objects, con
auto& ditherCmd2 = balloc.allocateFragment<psyqo::Prim::TPage>();
ditherCmd2.primitive.attr.setDithering(true);
ot.insert(ditherCmd2, ORDERING_TABLE_SIZE - 1);
writeFogRegisters();
Frustum frustum; m_currentCamera->ExtractFrustum(frustum);
int visibleCount = bvh.cullFrustum(frustum, m_visibleRefs, MAX_VISIBLE_TRIANGLES);
psyqo::Vec3 cameraPosition = computeCameraViewPos();
@@ -510,7 +504,7 @@ void psxsplash::Renderer::RenderWithRooms(eastl::vector<GameObject*>& objects,
auto& ditherCmd3 = balloc.allocateFragment<psyqo::Prim::TPage>();
ditherCmd3.primitive.attr.setDithering(true);
ot.insert(ditherCmd3, ORDERING_TABLE_SIZE - 1);
writeFogRegisters();
psyqo::Vec3 cameraPosition = computeCameraViewPos();
int32_t fogFarSZ = m_fog.fogFarSZ;
int32_t camX = m_currentCamera->GetPosition().x.raw();

View File

@@ -97,12 +97,9 @@ class Renderer final {
TriangleRef m_visibleRefs[MAX_VISIBLE_TRIANGLES];
int m_frameCount = 0;
void writeFogRegisters();
psyqo::Vec3 computeCameraViewPos();
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,
psyqo::OrderingTable<ORDERING_TABLE_SIZE>& ot,
psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE>& balloc);

View File

@@ -1,29 +0,0 @@
#include "sceneloader.hh"
#include "fileloader.hh"
namespace psxsplash {
bool SceneLoader::s_initialised = false;
bool SceneLoader::Init() {
// FileLoader::Get().scheduleInit() is task-based; for backward compat
// with the old sync Init() we rely on main.cpp running the init task
// before any LoadFile calls. This flag is set unconditionally so that
// IsPCdrvAvailable() remains useful.
s_initialised = true;
return true;
}
bool SceneLoader::IsPCdrvAvailable() {
return s_initialised;
}
uint8_t* SceneLoader::LoadFile(const char* filename, int& outSize) {
return FileLoader::Get().LoadFileSync(filename, outSize);
}
void SceneLoader::FreeFile(uint8_t* data) {
FileLoader::Get().FreeFile(data);
}
} // namespace psxsplash

View File

@@ -1,31 +0,0 @@
#pragma once
#include <stdint.h>
namespace psxsplash {
/**
* SceneLoader — backward-compatibility façade.
*
* All calls now delegate to the FileLoader singleton (selected at compile
* time by the LOADER build flag). New code should use FileLoader directly.
*/
class SceneLoader {
public:
/** Initialise the active FileLoader backend (PCdrv or CD-ROM). */
static bool Init();
/** Load a file synchronously. Uses FileLoader::Get().LoadFileSync(). */
static uint8_t* LoadFile(const char* filename, int& outSize);
/** Free previously loaded data. Uses FileLoader::Get().FreeFile(). */
static void FreeFile(uint8_t* data);
/** Returns true after a successful Init(). */
static bool IsPCdrvAvailable();
private:
static bool s_initialised;
};
} // namespace psxsplash

View File

@@ -6,6 +6,7 @@
#include "profiler.hh"
#include "renderer.hh"
#include "splashpack.hh"
#include "streq.hh"
#include "luaapi.hh"
#include "loadingscreen.hh"
@@ -163,24 +164,21 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc
m_playerHeight = sceneSetup.playerHeight;
// Load movement parameters from splashpack (v8+)
m_controls.setMoveSpeed(sceneSetup.moveSpeed);
m_controls.setSprintSpeed(sceneSetup.sprintSpeed);
m_playerRadius = (int32_t)sceneSetup.playerRadius.value;
if (m_playerRadius == 0) m_playerRadius = PLAYER_RADIUS; // fallback to default
if (m_playerRadius == 0) m_playerRadius = PLAYER_RADIUS;
m_jumpVelocityRaw = (int32_t)sceneSetup.jumpVelocity.value;
int32_t gravityRaw = (int32_t)sceneSetup.gravity.value;
m_gravityPerFrame = gravityRaw / 30; // Convert per-second² to per-frame velocity change
if (m_gravityPerFrame == 0 && gravityRaw > 0) m_gravityPerFrame = 1; // Ensure nonzero
m_gravityPerFrame = gravityRaw / 30;
if (m_gravityPerFrame == 0 && gravityRaw > 0) m_gravityPerFrame = 1;
m_velocityY = 0;
m_isGrounded = true;
m_lastFrameTime = 0;
m_deltaFrames = 1;
// Initialize collision system
m_collisionSystem.init();
// Register colliders from splashpack data
for (size_t i = 0; i < sceneSetup.colliders.size(); i++) {
SPLASHPACKCollider* collider = sceneSetup.colliders[i];
if (collider == nullptr) continue;
@@ -203,7 +201,6 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc
);
}
// Register trigger boxes from splashpack data
for (size_t i = 0; i < sceneSetup.triggerBoxes.size(); i++) {
SPLASHPACKTriggerBox* tb = sceneSetup.triggerBoxes[i];
if (tb == nullptr) continue;
@@ -219,9 +216,7 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc
m_collisionSystem.registerTriggerBox(bounds, tb->luaFileIndex);
}
// Load Lua files - order is important here. We need
// to load the Lua files before we register the game objects,
// as the game objects may reference Lua files by index.
for (int i = 0; i < m_luaFiles.size(); i++) {
auto luaFile = m_luaFiles[i];
L.LoadLuaFile(luaFile->luaCode, luaFile->length, i);
@@ -233,7 +228,6 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc
L.OnSceneCreationStart();
// Register game objects
for (auto object : m_gameObjects) {
L.RegisterGameObject(object);
}
@@ -255,34 +249,25 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc
void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
LuaAPI::IncrementFrameCount();
// Tick cutscene player (advance frame and apply tracks before rendering)
m_cutscenePlayer.tick();
// Delta-time measurement: count elapsed frames based on gpu timer
// PS1 NTSC frame = ~33333 microseconds (30fps vsync)
{
uint32_t now = gpu.now();
if (m_lastFrameTime != 0) {
uint32_t elapsed = now - m_lastFrameTime;
// 33333us per frame at 30fps. If >50000us, we dropped a frame.
m_deltaFrames = (elapsed > 50000) ? 2 : 1;
if (elapsed > 83000) m_deltaFrames = 3; // Two frames dropped
if (elapsed > 83000) m_deltaFrames = 3;
}
m_lastFrameTime = now;
}
uint32_t renderingStart = gpu.now();
auto& renderer = psxsplash::Renderer::GetInstance();
// Dispatch render path based on scene type.
// Interior scenes (type 1) use room/portal occlusion; exterior scenes use BVH culling.
if (m_sceneType == 1 && m_roomCount > 0 && m_rooms != nullptr) {
// Determine which room the camera is in for portal culling.
// During cutscene playback the camera may be elsewhere from the player,
// so use the actual camera position to find the room.
int camRoom = -1;
if (m_navRegions.isLoaded()) {
if (m_cutscenePlayer.isPlaying()) {
// Camera-driven: look up nav region from camera world position
auto& camPos = m_currentCamera.GetPosition();
uint16_t camRegion = m_navRegions.findRegion(camPos.x.value, camPos.z.value);
if (camRegion != NAV_NO_REGION) {
@@ -290,7 +275,6 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
if (ri != 0xFF) camRoom = (int)ri;
}
} else if (m_playerNavRegion != NAV_NO_REGION) {
// Normal gameplay: use cached player nav region
uint8_t ri = m_navRegions.getRoomIndex(m_playerNavRegion);
if (ri != 0xFF) camRoom = (int)ri;
}
@@ -308,10 +292,8 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_RENDERING, renderingTime);
#endif
// Collision detection
uint32_t collisionStart = gpu.now();
// Build player AABB from position + radius/height
AABB playerAABB;
{
psyqo::FixedPoint<12> r;
@@ -327,7 +309,6 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
psyqo::Vec3 pushBack;
int collisionCount = m_collisionSystem.detectCollisions(playerAABB, pushBack);
// Apply push-back to player position
{
psyqo::FixedPoint<12> zero;
if (pushBack.x != zero || pushBack.z != zero) {
@@ -426,9 +407,7 @@ void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
uint32_t navmeshStart = gpu.now();
if (!freecam) {
// Priority: WorldCollision + NavRegions (v7) > NavGrid (v5) > Legacy Navmesh
if (m_worldCollision.isLoaded()) {
// Move-and-slide against world geometry (XZ walls only)
psyqo::Vec3 slid = m_worldCollision.moveAndSlide(
oldPlayerPosition, m_playerPosition, m_playerRadius, 0xFF);
@@ -664,9 +643,6 @@ void psxsplash::SceneManager::loadScene(psyqo::GPU& gpu, int sceneIndex, bool is
char filename[32];
FileLoader::BuildSceneFilename(sceneIndex, filename, sizeof(filename));
// Blank BOTH framebuffers so the user doesn't see the BIOS screen
// or a frozen frame. FastFill ignores the scissor/DrawingArea so we
// can target any VRAM region directly.
psyqo::Prim::FastFill ff(psyqo::Color{.r = 0, .g = 0, .b = 0});
ff.rect = psyqo::Rect{0, 0, 320, 240};
gpu.sendPrimitive(ff);
@@ -674,7 +650,6 @@ void psxsplash::SceneManager::loadScene(psyqo::GPU& gpu, int sceneIndex, bool is
gpu.sendPrimitive(ff);
gpu.pumpCallbacks();
// Try to load a loading screen for the target scene
LoadingScreen loading;
if (s_font) {
if (loading.load(gpu, *s_font, sceneIndex)) {
@@ -742,9 +717,7 @@ void psxsplash::SceneManager::clearScene() {
// (bytecode, strings, tables, registry) in one shot via lua_close.
L.Shutdown();
// 2. Free vector BACKING STORAGE (not just contents).
// clear() only sets size=0 but keeps the allocated capacity.
// swap-with-empty releases the heap blocks so they can be coalesced.
// 2. Clear all vectors to free their heap storage (game objects, Lua files, names, etc)
{ eastl::vector<GameObject*> tmp; tmp.swap(m_gameObjects); }
{ eastl::vector<LuaFile*> tmp; tmp.swap(m_luaFiles); }
{ eastl::vector<const char*> tmp; tmp.swap(m_objectNames); }
@@ -775,12 +748,10 @@ void psxsplash::SceneManager::shrinkBuffer() {
if (m_liveDataSize == 0 || m_currentSceneData == nullptr) return;
uint8_t* oldBase = m_currentSceneData;
// Allocate the shrunk buffer. The volatile cast prevents the compiler
// from assuming operator new never returns NULL (it does with
// -fno-exceptions), which would let it optimize away the null check.
uint8_t* volatile newBaseV = new uint8_t[m_liveDataSize];
uint8_t* newBase = newBaseV;
if (!newBase) return; // Heap exhausted — keep the full buffer
if (!newBase) return;
__builtin_memcpy(newBase, oldBase, m_liveDataSize);
intptr_t delta = reinterpret_cast<intptr_t>(newBase) - reinterpret_cast<intptr_t>(oldBase);
@@ -823,11 +794,6 @@ void psxsplash::SceneManager::shrinkBuffer() {
m_uiSystem.relocate(delta);
// Re-key Lua registry entries for game objects. RegisterGameObject stored
// lightuserdata keys using the OLD buffer addresses. After relocation, the
// game object pointers changed but the registry keys are stale. Without
// this, Entity.Find/FindByIndex return nil (wrong key), and in release
// builds (-Os) the optimizer can turn this into a hard crash.
if (!m_gameObjects.empty()) {
L.RelocateGameObjects(
reinterpret_cast<GameObject**>(m_gameObjects.data()),
@@ -842,16 +808,11 @@ void psxsplash::SceneManager::shrinkBuffer() {
// OBJECT NAME LOOKUP
// ============================================================================
// Inline streq (no libc on bare-metal PS1)
static bool name_eq(const char* a, const char* b) {
while (*a && *b) { if (*a++ != *b++) return false; }
return *a == *b;
}
psxsplash::GameObject* psxsplash::SceneManager::findObjectByName(const char* name) const {
if (!name || m_objectNames.empty()) return nullptr;
for (size_t i = 0; i < m_objectNames.size() && i < m_gameObjects.size(); i++) {
if (m_objectNames[i] && name_eq(m_objectNames[i], name)) {
if (m_objectNames[i] && streq(m_objectNames[i], name)) {
return m_gameObjects[i];
}
}
@@ -861,7 +822,7 @@ psxsplash::GameObject* psxsplash::SceneManager::findObjectByName(const char* nam
int psxsplash::SceneManager::findAudioClipByName(const char* name) const {
if (!name || m_audioClipNames.empty()) return -1;
for (size_t i = 0; i < m_audioClipNames.size(); i++) {
if (m_audioClipNames[i] && name_eq(m_audioClipNames[i], name)) {
if (m_audioClipNames[i] && streq(m_audioClipNames[i], name)) {
return static_cast<int>(i);
}
}

View File

@@ -139,7 +139,6 @@ class SceneManager {
int m_cutsceneCount = 0;
CutscenePlayer m_cutscenePlayer;
// UI system (v13+)
UISystem m_uiSystem;
#ifdef PSXSPLASH_MEMOVERLAY
MemOverlay m_memOverlay;
@@ -154,7 +153,6 @@ class SceneManager {
psyqo::FixedPoint<12, uint16_t> m_playerHeight;
// Movement physics (v8+)
int32_t m_playerRadius; // Collision radius in fp12 (replaces hardcoded PLAYER_RADIUS)
int32_t m_velocityY; // Vertical velocity in fp12 per second (negative = up)
int32_t m_gravityPerFrame; // Gravity velocity change per frame (fp12)
@@ -182,5 +180,4 @@ class SceneManager {
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

@@ -12,20 +12,13 @@
#include "cutscene.hh"
#include "lua.h"
#include "mesh.hh"
#include "streq.hh"
#include "worldcollision.hh"
#include "navregion.hh"
#include "renderer.hh"
namespace psxsplash {
// Bare-metal string compare (no libc)
static bool sp_streq(const char* a, const char* b) {
while (*a && *b) {
if (*a++ != *b++) return false;
}
return *a == *b;
}
struct SPLASHPACKFileHeader {
char magic[2];
uint16_t version;
@@ -99,7 +92,6 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
setup.playerStartRotation = header->playerStartRot;
setup.playerHeight = header->playerHeight;
// Movement parameters (v8+)
setup.moveSpeed.value = header->moveSpeed;
setup.sprintSpeed.value = header->sprintSpeed;
setup.jumpVelocity.value = header->jumpVelocity;
@@ -120,7 +112,6 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += sizeof(psxsplash::LuaFile);
}
// sceneLuaFileIndex is stored as uint16_t in header; 0xFFFF means "no scene script" (-1)
setup.sceneLuaFileIndex = (header->sceneLuaFileIndex == 0xFFFF) ? -1 : (int)header->sceneLuaFileIndex;
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
@@ -130,14 +121,12 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += sizeof(psxsplash::GameObject);
}
// Read collision data (after GameObjects)
for (uint16_t i = 0; i < header->colliderCount; i++) {
psxsplash::SPLASHPACKCollider *collider = reinterpret_cast<psxsplash::SPLASHPACKCollider *>(cursor);
setup.colliders.push_back(collider);
cursor += sizeof(psxsplash::SPLASHPACKCollider);
}
// Read trigger boxes (after colliders)
setup.triggerBoxes.reserve(header->triggerBoxCount);
for (uint16_t i = 0; i < header->triggerBoxCount; i++) {
psxsplash::SPLASHPACKTriggerBox *tb = reinterpret_cast<psxsplash::SPLASHPACKTriggerBox *>(cursor);
@@ -145,7 +134,6 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += sizeof(psxsplash::SPLASHPACKTriggerBox);
}
// BVH data
if (header->bvhNodeCount > 0) {
BVHNode* bvhNodes = reinterpret_cast<BVHNode*>(cursor);
cursor += header->bvhNodeCount * sizeof(BVHNode);
@@ -157,28 +145,24 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
triangleRefs, header->bvhTriangleRefCount);
}
// Interactables
for (uint16_t i = 0; i < header->interactableCount; i++) {
psxsplash::Interactable *interactable = reinterpret_cast<psxsplash::Interactable *>(cursor);
setup.interactables.push_back(interactable);
cursor += sizeof(psxsplash::Interactable);
}
// World collision soup
if (header->worldCollisionMeshCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.worldCollision.initializeFromData(cursor));
}
// Nav regions
if (header->navRegionCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.navRegions.initializeFromData(cursor));
}
// Room/portal data (interior scenes)
if (header->roomCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
@@ -218,7 +202,6 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
uint8_t nameLen = *nameData++;
const char* nameStr = reinterpret_cast<const char*>(nameData);
// Names are stored as length-prefixed, null-terminated strings
setup.objectNames.push_back(nameStr);
nameData += nameLen + 1; // +1 for null terminator
}
@@ -318,7 +301,7 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
} else {
for (size_t oi = 0; oi < setup.objectNames.size(); oi++) {
if (setup.objectNames[oi] &&
sp_streq(setup.objectNames[oi], objName)) {
streq(setup.objectNames[oi], objName)) {
track.target = setup.objects[oi];
break;
}
@@ -343,7 +326,6 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
}
}
// Read UI canvas data (version 13+)
if (header->version >= 13) {
setup.uiCanvasCount = header->uiCanvasCount;
setup.uiFontCount = header->uiFontCount;

View File

@@ -51,7 +51,6 @@ struct SplashpackSceneSetup {
// New component arrays
eastl::vector<Interactable *> interactables;
// Object name table (v9+): parallel to objects, points into splashpack data
eastl::vector<const char *> objectNames;
// Audio clips (v10+): ADPCM data with metadata
@@ -64,12 +63,11 @@ struct SplashpackSceneSetup {
};
eastl::vector<AudioClipSetup> audioClips;
// Audio clip name table (v10+): parallel to audioClips, points into splashpack data
eastl::vector<const char*> audioClipNames;
BVHManager bvh; // Spatial acceleration structure for culling
WorldCollision worldCollision; // Triangle-level world collision (v7+)
NavRegionSystem navRegions; // Convex region navigation (v7+)
WorldCollision worldCollision;
NavRegionSystem navRegions;
psyqo::GTE::PackedVec3 playerStartPosition;
psyqo::GTE::PackedVec3 playerStartRotation;
psyqo::FixedPoint<12, uint16_t> playerHeight;
@@ -82,7 +80,6 @@ struct SplashpackSceneSetup {
uint8_t fogR = 0, fogG = 0, fogB = 0;
uint8_t fogDensity = 5;
// Room/portal data (v11+, interior scenes only)
const RoomData* rooms = nullptr;
uint16_t roomCount = 0;
const PortalData* portals = nullptr;
@@ -90,25 +87,20 @@ struct SplashpackSceneSetup {
const TriangleRef* roomTriRefs = nullptr;
uint16_t roomTriRefCount = 0;
// Movement parameters (v8+)
psyqo::FixedPoint<12, uint16_t> moveSpeed; // Per-frame speed constant (fp12)
psyqo::FixedPoint<12, uint16_t> sprintSpeed; // Per-frame sprint constant (fp12)
psyqo::FixedPoint<12, uint16_t> jumpVelocity; // Per-second initial velocity (fp12)
psyqo::FixedPoint<12, uint16_t> gravity; // Per-second² acceleration (fp12)
psyqo::FixedPoint<12, uint16_t> playerRadius; // Collision radius (fp12)
// Cutscenes (version 12+)
Cutscene loadedCutscenes[MAX_CUTSCENES];
int cutsceneCount = 0;
// UI system (v13+)
uint16_t uiCanvasCount = 0;
uint8_t uiFontCount = 0;
uint32_t uiTableOffset = 0;
// Buffer management (v15+): byte offset where pixel/audio bulk data begins.
// Everything before this offset is needed at runtime; everything after can
// be freed once VRAM/SPU uploads are done.
uint32_t liveDataSize = 0;
};
@@ -117,4 +109,4 @@ class SplashPackLoader {
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

View File

@@ -6,17 +6,10 @@
#include <psyqo/primitives/rectangles.hh>
#include <psyqo/primitives/sprites.hh>
#include <psyqo/primitives/triangles.hh>
#include "streq.hh"
namespace psxsplash {
// Bare-metal string compare (no libc)
static bool ui_streq(const char* a, const char* b) {
while (*a && *b) {
if (*a++ != *b++) return false;
}
return *a == *b;
}
// ============================================================================
// Init
// ============================================================================
@@ -521,7 +514,7 @@ void UISystem::renderProportionalText(int fontIdx, int16_t x, int16_t y,
int UISystem::findCanvas(const char* name) const {
if (!name) return -1;
for (int i = 0; i < m_canvasCount; i++) {
if (m_canvases[i].name && ui_streq(m_canvases[i].name, name))
if (m_canvases[i].name && streq(m_canvases[i].name, name))
return i;
}
return -1;
@@ -546,7 +539,7 @@ int UISystem::findElement(int canvasIdx, const char* name) const {
if (canvasIdx < 0 || canvasIdx >= m_canvasCount || !name) return -1;
const UICanvas& cv = m_canvases[canvasIdx];
for (int i = 0; i < cv.elementCount; i++) {
if (cv.elements[i].name && ui_streq(cv.elements[i].name, name)) {
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;

View File

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

View File

@@ -3,16 +3,6 @@
#include <psyqo/fixed-point.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 {

Binary file not shown.