Compare commits

..

10 Commits

53 changed files with 8926 additions and 787 deletions

View File

@@ -0,0 +1 @@
ˆ¨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

0
.sentry-native/metadata Normal file
View File

BIN
.sentry-native/settings.dat Normal file

Binary file not shown.

View File

@@ -7,12 +7,20 @@ src/renderer.cpp \
src/splashpack.cpp \ src/splashpack.cpp \
src/camera.cpp \ src/camera.cpp \
src/gtemath.cpp \ src/gtemath.cpp \
src/navmesh.cpp \ src/worldcollision.cpp \
src/navregion.cpp \
src/triclip.cpp \
src/lua.cpp \ src/lua.cpp \
output.o src/luaapi.cpp \
src/scenemanager.cpp \
src/sceneloader.cpp \
src/audiomanager.cpp \
src/controls.cpp \
src/profiler.cpp \
src/collision.cpp \
src/bvh.cpp
CPPFLAGS += -DPCDRV_SUPPORT=1
include third_party/nugget/psyqo/psyqo.mk
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
%.o: %.bin
$(PREFIX)-objcopy -I binary --set-section-alignment .data=4 --rename-section .data=.rodata,alloc,load,readonly,data,contents -O $(FORMAT) -B mips $< $@

BIN
build_output.txt Normal file

Binary file not shown.

Binary file not shown.

199
src/audiomanager.cpp Normal file
View File

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

97
src/audiomanager.hh Normal file
View File

@@ -0,0 +1,97 @@
#pragma once
#include <stdint.h>
namespace psxsplash {
/// Maximum number of audio clips that can be loaded in a scene
static constexpr int MAX_AUDIO_CLIPS = 32;
/// Maximum SPU voices (hardware limit)
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_END = 0x10000;
/// 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;
/// Descriptor for a loaded audio clip in SPU RAM
struct AudioClip {
uint32_t spuAddr; // Byte address in SPU RAM
uint32_t size; // Size of ADPCM data in bytes
uint16_t sampleRate; // Original sample rate in Hz
bool loop; // Whether this clip should loop
bool loaded; // Whether this slot is valid
};
/// 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()
/// loadClip(index, data, size, rate, loop) -> bool
/// play(clipIndex) -> channel
/// play(clipIndex, volume, pan) -> channel
/// stopVoice(channel)
/// stopAll()
/// setVoiceVolume(channel, vol, pan)
///
/// Volume is 0-128 (0=silent, 128=max). Pan is 0-127 (64=center).
class AudioManager {
public:
/// Initialize SPU hardware and reset state
void init();
/// Upload ADPCM data to SPU RAM and register as clip index.
/// Data must be 16-byte aligned (SPU ADPCM block size). Returns true on success.
bool loadClip(int clipIndex, const uint8_t* adpcmData, uint32_t sizeBytes,
uint16_t sampleRate, bool loop);
/// Play a clip by index. Returns channel (0-23), or -1 if full.
/// Volume: 0-128 (128=max). Pan: 0 (left) to 127 (right), 64 = center.
int play(int clipIndex, int volume = 128, int pan = 64);
/// Stop a specific channel (returned from play())
void stopVoice(int channel);
/// Stop all playing channels
void stopAll();
/// Set volume/pan on a playing channel
void setVoiceVolume(int channel, int volume, int pan = 64);
/// Get total SPU RAM used by loaded clips (for visualization)
uint32_t getUsedSPURam() const { return m_nextAddr - SPU_RAM_START; }
/// Get total SPU RAM available
uint32_t getTotalSPURam() const { return SPU_RAM_END - SPU_RAM_START; }
/// Get number of loaded clips
int getLoadedClipCount() const;
/// Reset all clips and free SPU RAM (call on scene unload)
void reset();
private:
/// Convert 0-128 volume to hardware 0-0x3FFF (fixed-volume mode)
static uint16_t volToHw(int v);
AudioClip m_clips[MAX_AUDIO_CLIPS];
uint32_t m_nextAddr = SPU_RAM_START; // Bump allocator for SPU RAM
};
} // namespace psxsplash

143
src/bvh.cpp Normal file
View File

@@ -0,0 +1,143 @@
#include "bvh.hh"
namespace psxsplash {
void BVHManager::initialize(const BVHNode* nodes, uint16_t nodeCount,
const TriangleRef* triangleRefs, uint16_t triangleRefCount) {
m_nodes = nodes;
m_nodeCount = nodeCount;
m_triangleRefs = triangleRefs;
m_triangleRefCount = triangleRefCount;
}
const uint8_t* BVHManager::initializeFromData(const uint8_t* data, uint16_t nodeCount, uint16_t triangleRefCount) {
if (data == nullptr || nodeCount == 0) {
m_nodes = nullptr;
m_triangleRefs = nullptr;
m_nodeCount = 0;
m_triangleRefCount = 0;
return data;
}
// 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,
TriangleRef* outRefs,
int maxRefs) const {
if (!isLoaded() || m_nodeCount == 0) return 0;
return traverseFrustum(0, frustum, outRefs, 0, maxRefs);
}
int BVHManager::traverseFrustum(int nodeIndex,
const Frustum& frustum,
TriangleRef* outRefs,
int currentCount,
int maxRefs) const {
if (nodeIndex < 0 || nodeIndex >= m_nodeCount) return currentCount;
if (currentCount >= maxRefs) return currentCount;
const BVHNode& node = m_nodes[nodeIndex];
// 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;
}
int BVHManager::queryRegion(int32_t minX, int32_t minY, int32_t minZ,
int32_t maxX, int32_t maxY, int32_t maxZ,
TriangleRef* outRefs,
int maxRefs) const {
if (!isLoaded() || m_nodeCount == 0) return 0;
return traverseRegion(0, minX, minY, minZ, maxX, maxY, maxZ, outRefs, 0, maxRefs);
}
int BVHManager::traverseRegion(int nodeIndex,
int32_t qMinX, int32_t qMinY, int32_t qMinZ,
int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ,
TriangleRef* outRefs,
int currentCount,
int maxRefs) const {
if (nodeIndex < 0 || nodeIndex >= m_nodeCount) return currentCount;
if (currentCount >= maxRefs) return currentCount;
const BVHNode& node = m_nodes[nodeIndex];
// 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;
}
bool BVHManager::aabbOverlap(const BVHNode& node,
int32_t qMinX, int32_t qMinY, int32_t qMinZ,
int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ) {
// Check for separation on any axis
if (node.maxX < qMinX || node.minX > qMaxX) return false;
if (node.maxY < qMinY || node.minY > qMaxY) return false;
if (node.maxZ < qMinZ || node.minZ > qMaxZ) return false;
return true; // Overlapping
}
} // namespace psxsplash

178
src/bvh.hh Normal file
View File

@@ -0,0 +1,178 @@
#pragma once
#include <stdint.h>
#include <psyqo/fixed-point.hh>
#include <psyqo/vector.hh>
namespace psxsplash {
/// Triangle reference - points to a specific triangle in a specific object
struct TriangleRef {
uint16_t objectIndex;
uint16_t triangleIndex;
};
static_assert(sizeof(TriangleRef) == 4, "TriangleRef must be 4 bytes");
/// BVH Node - stored in binary file
/// 32 bytes per node for cache-friendly traversal
struct BVHNode {
// AABB bounds in fixed-point 20.12 format
int32_t minX, minY, minZ; // 12 bytes
int32_t maxX, maxY, maxZ; // 12 bytes
// Child indices (0xFFFF = no child / leaf indicator)
uint16_t leftChild; // 2 bytes
uint16_t rightChild; // 2 bytes
// Triangle data (only valid for leaf nodes)
uint16_t firstTriangle; // 2 bytes - index into triangle ref array
uint16_t triangleCount; // 2 bytes
/// Check if this is a leaf node
bool isLeaf() const {
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 {
// Find the corner most in the direction of the plane normal (p-vertex)
int32_t px = (nx >= 0) ? maxX : minX;
int32_t py = (ny >= 0) ? maxY : minY;
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;
return (dot + d) >= 0;
}
};
static_assert(sizeof(BVHNode) == 32, "BVHNode must be 32 bytes");
/// BVH Tree header in binary file
struct BVHHeader {
uint16_t nodeCount;
uint16_t triangleRefCount;
};
static_assert(sizeof(BVHHeader) == 4, "BVHHeader must be 4 bytes");
/// Frustum planes for culling (6 planes)
struct Frustum {
// Each plane: nx, ny, nz (normal), d (distance)
// All in fixed-point 20.12 format
struct Plane {
int32_t nx, ny, nz, d;
};
Plane planes[6]; // Near, Far, Left, Right, Top, Bottom
/// Test if AABB is visible (not culled by all planes)
bool testAABB(const BVHNode& node) const {
for (int i = 0; i < 6; i++) {
if (!node.testPlane(planes[i].nx, planes[i].ny, planes[i].nz, planes[i].d)) {
return false; // Completely outside this plane
}
}
return true; // Potentially visible
}
};
/// BVH Manager - handles traversal and culling
class BVHManager {
public:
/// Initialize from separate pointers (used by splashpack loader)
void initialize(const BVHNode* nodes, uint16_t nodeCount,
const TriangleRef* triangleRefs, uint16_t triangleRefCount);
/// 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
/// Uses frustum culling to skip invisible branches
/// Returns number of visible triangle refs
int cullFrustum(const Frustum& frustum,
TriangleRef* outRefs,
int maxRefs) const;
/// Simpler traversal - collect all triangles in a region
/// Useful for collision queries
int queryRegion(int32_t minX, int32_t minY, int32_t minZ,
int32_t maxX, int32_t maxY, int32_t maxZ,
TriangleRef* outRefs,
int maxRefs) const;
/// Get node count
int getNodeCount() const { return m_nodeCount; }
/// Get triangle ref count
int getTriangleRefCount() const { return m_triangleRefCount; }
/// Check if BVH is loaded
bool isLoaded() const { return m_nodes != nullptr; }
private:
const BVHNode* m_nodes = nullptr;
const TriangleRef* m_triangleRefs = nullptr;
uint16_t m_nodeCount = 0;
uint16_t m_triangleRefCount = 0;
/// Recursive frustum culling traversal
int traverseFrustum(int nodeIndex,
const Frustum& frustum,
TriangleRef* outRefs,
int currentCount,
int maxRefs) const;
/// Recursive region query traversal
int traverseRegion(int nodeIndex,
int32_t qMinX, int32_t qMinY, int32_t qMinZ,
int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ,
TriangleRef* outRefs,
int currentCount,
int maxRefs) const;
/// Test if two AABBs overlap
static bool aabbOverlap(const BVHNode& node,
int32_t qMinX, int32_t qMinY, int32_t qMinZ,
int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ);
};
// ── Room/portal data for interior scene occlusion ──
/// Per-room data loaded from splashpack v11+.
/// AABB for point-in-room tests plus a range into the room triangle-ref array.
struct RoomData {
int32_t aabbMinX, aabbMinY, aabbMinZ; // 12 bytes
int32_t aabbMaxX, aabbMaxY, aabbMaxZ; // 12 bytes
uint16_t firstTriRef; // 2 bytes - index into room tri-ref array
uint16_t triRefCount; // 2 bytes
uint32_t pad; // 4 bytes (alignment)
};
static_assert(sizeof(RoomData) == 32, "RoomData must be 32 bytes");
/// Per-portal data connecting two rooms.
/// Center position is in fixed-point world/GTE space (20.12).
/// halfW/halfH define the portal opening size.
/// Normal, right, and up define the portal's orientation in world space.
/// Corner vertices are computed as: center +/- right*halfW +/- up*halfH.
struct PortalData {
uint16_t roomA; // 2 bytes
uint16_t roomB; // 2 bytes
int32_t centerX, centerY, centerZ; // 12 bytes - portal center (20.12 fp)
int16_t halfW; // 2 bytes - half-width in GTE units (4.12 fp)
int16_t halfH; // 2 bytes - half-height in GTE units (4.12 fp)
int16_t normalX, normalY, normalZ; // 6 bytes - facing direction (4.12 fp unit vector)
int16_t pad; // 2 bytes - alignment
int16_t rightX, rightY, rightZ; // 6 bytes - local right axis (4.12 fp unit vector)
int16_t upX, upY, upZ; // 6 bytes - local up axis (4.12 fp unit vector)
};
static_assert(sizeof(PortalData) == 40, "PortalData must be 40 bytes");
} // namespace psxsplash

View File

@@ -35,3 +35,123 @@ 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 {
// =========================================================================
// 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; // 320/2
constexpr int32_t SCREEN_HALF_HEIGHT = 120; // 240/2
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 rightY = m_rotationMatrix.vs[0].y.raw();
int32_t rightZ = m_rotationMatrix.vs[0].z.raw();
int32_t upX = m_rotationMatrix.vs[1].x.raw();
int32_t upY = m_rotationMatrix.vs[1].y.raw();
int32_t upZ = m_rotationMatrix.vs[1].z.raw();
int32_t fwdX = m_rotationMatrix.vs[2].x.raw();
int32_t fwdY = m_rotationMatrix.vs[2].y.raw();
int32_t fwdZ = m_rotationMatrix.vs[2].z.raw();
int32_t camX = m_position.x.raw();
int32_t camY = m_position.y.raw();
int32_t camZ = m_position.z.raw();
// =========================================================================
// PLANE 0: NEAR PLANE
// Normal points FORWARD (into visible space)
// =========================================================================
frustum.planes[0].nx = fwdX;
frustum.planes[0].ny = fwdY;
frustum.planes[0].nz = fwdZ;
int64_t fwdDotCam = ((int64_t)fwdX * camX + (int64_t)fwdY * camY + (int64_t)fwdZ * camZ) >> 12;
frustum.planes[0].d = -fwdDotCam;
// =========================================================================
// 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].ny = -fwdY;
frustum.planes[1].nz = -fwdZ;
frustum.planes[1].d = fwdDotCam + (4096 * 2000); // 2000 units far plane
// =========================================================================
// 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].ny = ((int64_t)rightY * H + (int64_t)fwdY * SCREEN_HALF_WIDTH) >> 12;
frustum.planes[2].nz = ((int64_t)rightZ * H + (int64_t)fwdZ * SCREEN_HALF_WIDTH) >> 12;
frustum.planes[2].d = -(((int64_t)frustum.planes[2].nx * camX +
(int64_t)frustum.planes[2].ny * camY +
(int64_t)frustum.planes[2].nz * camZ) >> 12);
// 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].ny = ((int64_t)(-rightY) * H + (int64_t)fwdY * SCREEN_HALF_WIDTH) >> 12;
frustum.planes[3].nz = ((int64_t)(-rightZ) * H + (int64_t)fwdZ * SCREEN_HALF_WIDTH) >> 12;
frustum.planes[3].d = -(((int64_t)frustum.planes[3].nx * camX +
(int64_t)frustum.planes[3].ny * camY +
(int64_t)frustum.planes[3].nz * camZ) >> 12);
// 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].ny = ((int64_t)upY * H + (int64_t)fwdY * SCREEN_HALF_HEIGHT) >> 12;
frustum.planes[4].nz = ((int64_t)upZ * H + (int64_t)fwdZ * SCREEN_HALF_HEIGHT) >> 12;
frustum.planes[4].d = -(((int64_t)frustum.planes[4].nx * camX +
(int64_t)frustum.planes[4].ny * camY +
(int64_t)frustum.planes[4].nz * camZ) >> 12);
// 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].ny = ((int64_t)(-upY) * H + (int64_t)fwdY * SCREEN_HALF_HEIGHT) >> 12;
frustum.planes[5].nz = ((int64_t)(-upZ) * H + (int64_t)fwdZ * SCREEN_HALF_HEIGHT) >> 12;
frustum.planes[5].d = -(((int64_t)frustum.planes[5].nx * camX +
(int64_t)frustum.planes[5].ny * camY +
(int64_t)frustum.planes[5].nz * camZ) >> 12);
}

View File

@@ -4,6 +4,8 @@
#include <psyqo/matrix.hh> #include <psyqo/matrix.hh>
#include <psyqo/trigonometry.hh> #include <psyqo/trigonometry.hh>
#include "bvh.hh"
namespace psxsplash { namespace psxsplash {
// Camera class for managing 3D position and rotation. // Camera class for managing 3D position and rotation.
@@ -21,6 +23,10 @@ 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;
private: private:
psyqo::Matrix33 m_rotationMatrix; psyqo::Matrix33 m_rotationMatrix;
psyqo::Trig<> m_trig; psyqo::Trig<> m_trig;

468
src/collision.cpp Normal file
View File

@@ -0,0 +1,468 @@
#include "collision.hh"
#include "scenemanager.hh"
#include <psyqo/fixed-point.hh>
// Helper type alias for brevity
using FP = psyqo::FixedPoint<12>;
namespace psxsplash {
// Static member initialization
psyqo::FixedPoint<12> SpatialGrid::WORLD_MIN = FP(-16);
psyqo::FixedPoint<12> SpatialGrid::WORLD_MAX = FP(16);
psyqo::FixedPoint<12> SpatialGrid::CELL_SIZE = FP(4); // (32 / 8) = 4
// AABB expand implementation
void AABB::expand(const psyqo::Vec3& delta) {
psyqo::FixedPoint<12> zero;
if (delta.x > zero) max.x = max.x + delta.x;
else min.x = min.x + delta.x;
if (delta.y > zero) max.y = max.y + delta.y;
else min.y = min.y + delta.y;
if (delta.z > zero) max.z = max.z + delta.z;
else min.z = min.z + delta.z;
}
// ============================================================================
// SpatialGrid Implementation
// ============================================================================
void SpatialGrid::clear() {
for (int i = 0; i < CELL_COUNT; i++) {
m_cells[i].count = 0;
}
}
void SpatialGrid::worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const {
// Clamp position to world bounds
auto px = pos.x;
auto py = pos.y;
auto pz = pos.z;
if (px < WORLD_MIN) px = WORLD_MIN;
if (px > WORLD_MAX) px = WORLD_MAX;
if (py < WORLD_MIN) py = WORLD_MIN;
if (py > WORLD_MAX) py = WORLD_MAX;
if (pz < WORLD_MIN) pz = WORLD_MIN;
if (pz > WORLD_MAX) pz = WORLD_MAX;
// Convert to grid coordinates (0 to GRID_SIZE-1)
// Using integer division after scaling
gx = ((px - WORLD_MIN) / CELL_SIZE).integer();
gy = ((py - WORLD_MIN) / CELL_SIZE).integer();
gz = ((pz - WORLD_MIN) / CELL_SIZE).integer();
// Clamp to valid range
if (gx < 0) gx = 0;
if (gx >= GRID_SIZE) gx = GRID_SIZE - 1;
if (gy < 0) gy = 0;
if (gy >= GRID_SIZE) gy = GRID_SIZE - 1;
if (gz < 0) gz = 0;
if (gz >= GRID_SIZE) gz = GRID_SIZE - 1;
}
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) {
// Get grid range for this AABB
int minGx, minGy, minGz;
int maxGx, maxGy, maxGz;
worldToGrid(bounds.min, minGx, minGy, minGz);
worldToGrid(bounds.max, maxGx, maxGy, maxGz);
// Insert into all overlapping cells
for (int gz = minGz; gz <= maxGz; gz++) {
for (int gy = minGy; gy <= maxGy; gy++) {
for (int gx = minGx; gx <= maxGx; gx++) {
int cellIndex = gx + gy * GRID_SIZE + gz * GRID_SIZE * GRID_SIZE;
Cell& cell = m_cells[cellIndex];
if (cell.count < MAX_OBJECTS_PER_CELL) {
cell.objectIndices[cell.count++] = objectIndex;
}
// If cell is full, object won't be in this cell (may miss collisions)
// This is a tradeoff for memory/performance
}
}
}
}
int SpatialGrid::queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const {
int resultCount = 0;
// Get grid range for query AABB
int minGx, minGy, minGz;
int maxGx, maxGy, maxGz;
worldToGrid(bounds.min, minGx, minGy, minGz);
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; // Objects 0-31
uint32_t addedMaskHigh = 0; // Objects 32-63
// Query all overlapping cells
for (int gz = minGz; gz <= maxGz; gz++) {
for (int gy = minGy; gy <= maxGy; gy++) {
for (int gx = minGx; gx <= maxGx; gx++) {
int cellIndex = gx + gy * GRID_SIZE + gz * GRID_SIZE * GRID_SIZE;
const Cell& cell = m_cells[cellIndex];
for (int i = 0; i < cell.count; i++) {
uint16_t objIndex = cell.objectIndices[i];
// Skip if already added (using bitmask for objects 0-63)
if (objIndex < 32) {
uint32_t bit = 1U << objIndex;
if (addedMaskLow & bit) continue;
addedMaskLow |= bit;
} else if (objIndex < 64) {
uint32_t bit = 1U << (objIndex - 32);
if (addedMaskHigh & bit) continue;
addedMaskHigh |= bit;
}
if (resultCount < maxResults) {
output[resultCount++] = objIndex;
}
}
}
}
}
return resultCount;
}
// ============================================================================
// CollisionSystem Implementation
// ============================================================================
void CollisionSystem::init() {
reset();
}
void CollisionSystem::reset() {
m_colliderCount = 0;
m_resultCount = 0;
m_triggerPairCount = 0;
m_grid.clear();
}
void CollisionSystem::registerCollider(uint16_t gameObjectIndex, const AABB& localBounds,
CollisionType type, CollisionMask mask) {
if (m_colliderCount >= MAX_COLLIDERS) {
// Out of collider slots
return;
}
CollisionData& data = m_colliders[m_colliderCount++];
data.bounds = localBounds; // Will be transformed in updateCollider
data.type = type;
data.layerMask = mask;
data.flags = 0;
data.gridCell = 0;
data.gameObjectIndex = gameObjectIndex;
}
void CollisionSystem::updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position,
const psyqo::Matrix33& rotation) {
// Find the collider for this object
for (int i = 0; i < m_colliderCount; i++) {
if (m_colliders[i].gameObjectIndex == gameObjectIndex) {
// For now, just translate the AABB (no rotation support for AABBs)
// TODO: Compute rotated AABB if needed
// 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;
}
}
}
int CollisionSystem::detectCollisions() {
m_resultCount = 0;
// Clear and rebuild spatial grid
m_grid.clear();
for (int i = 0; i < m_colliderCount; i++) {
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
for (int i = 0; i < m_triggerPairCount; i++) {
TriggerPair& pair = m_triggerPairs[i];
pair.framesSinceContact++;
// If no contact for several frames, trigger exit
if (pair.framesSinceContact > 2 && pair.state != 2) {
pair.state = 2; // Exiting
}
}
return m_resultCount;
}
bool CollisionSystem::testAABB(const AABB& a, const AABB& b,
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.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;
// Calculate penetration on each axis
auto overlapX1 = a.max.x - b.min.x;
auto overlapX2 = b.max.x - a.min.x;
auto overlapY1 = a.max.y - b.min.y;
auto overlapY2 = b.max.y - a.min.y;
auto overlapZ1 = a.max.z - b.min.z;
auto overlapZ2 = b.max.z - a.min.z;
// Find minimum overlap axis
auto minOverlapX = (overlapX1 < overlapX2) ? overlapX1 : overlapX2;
auto minOverlapY = (overlapY1 < overlapY2) ? overlapY1 : overlapY2;
auto minOverlapZ = (overlapZ1 < overlapZ2) ? overlapZ1 : overlapZ2;
// Constants for normals
const FP zero(0);
const FP one(1);
const FP negOne(-1);
// Determine separation axis (axis with least penetration)
if (minOverlapX <= minOverlapY && minOverlapX <= minOverlapZ) {
penetration = minOverlapX;
normal = psyqo::Vec3{(overlapX1 < overlapX2) ? negOne : one, zero, zero};
} else if (minOverlapY <= minOverlapZ) {
penetration = minOverlapY;
normal = psyqo::Vec3{zero, (overlapY1 < overlapY2) ? negOne : one, zero};
} else {
penetration = minOverlapZ;
normal = psyqo::Vec3{zero, zero, (overlapZ1 < overlapZ2) ? negOne : one};
}
return true;
}
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

239
src/collision.hh Normal file
View File

@@ -0,0 +1,239 @@
#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/vector.hh>
#include <EASTL/vector.h>
#include "gameobject.hh"
namespace psxsplash {
// Forward declarations
class SceneManager;
/**
* Collision type flags - matches Unity PSXCollisionType enum
*/
enum class CollisionType : uint8_t {
None = 0,
Solid = 1, // Blocks movement
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;
/**
* Axis-Aligned Bounding Box in fixed-point
* Used for broadphase and narrowphase collision
*/
struct AABB {
psyqo::Vec3 min;
psyqo::Vec3 max;
// Check if this AABB intersects another
bool intersects(const AABB& other) const {
return (min.x <= other.max.x && max.x >= other.min.x) &&
(min.y <= other.max.y && max.y >= other.min.y) &&
(min.z <= other.max.z && max.z >= other.min.z);
}
// Check if a point is inside this AABB
bool contains(const psyqo::Vec3& point) const {
return (point.x >= min.x && point.x <= max.x) &&
(point.y >= min.y && point.y <= max.y) &&
(point.z >= min.z && point.z <= max.z);
}
// 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);
};
static_assert(sizeof(AABB) == 24, "AABB must be 24 bytes (2x Vec3)");
/**
* Collision data for a single object
* Stored separately from GameObject for cache efficiency
*/
struct CollisionData {
AABB bounds; // World-space AABB (24 bytes)
CollisionType type; // Collision behavior (1 byte)
CollisionMask layerMask; // Which layers this collides with (1 byte)
uint8_t flags; // Additional flags (1 byte)
uint8_t gridCell; // Current spatial grid cell (1 byte)
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 {
uint16_t objectA; // First object index
uint16_t objectB; // Second object index
psyqo::Vec3 normal; // Collision normal (from A to B)
psyqo::FixedPoint<12> penetration; // Penetration depth
};
/**
* Trigger state for tracking enter/stay/exit
*/
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;
};
/**
* Spatial Grid for broadphase collision
* Divides world into fixed-size cells for fast overlap queries
*/
class SpatialGrid {
public:
// Grid configuration
static constexpr int GRID_SIZE = 8; // 8x8x8 grid
static constexpr int CELL_COUNT = GRID_SIZE * GRID_SIZE * GRID_SIZE;
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_MAX;
static psyqo::FixedPoint<12> CELL_SIZE;
struct Cell {
uint16_t objectIndices[MAX_OBJECTS_PER_CELL];
uint8_t count;
uint8_t padding[3];
};
// Clear all cells
void clear();
// Insert an object into the grid
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;
// Get cell index for a position
int getCellIndex(const psyqo::Vec3& pos) const;
private:
Cell m_cells[CELL_COUNT];
// Convert world position to grid coordinates
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 {
public:
static constexpr int MAX_COLLIDERS = 64;
static constexpr int MAX_TRIGGERS = 32;
static constexpr int MAX_COLLISION_RESULTS = 32;
CollisionSystem() = default;
// Initialize the system
void init();
// Reset for new scene
void reset();
// Register a collider (called during scene load)
void registerCollider(uint16_t gameObjectIndex, const AABB& localBounds,
CollisionType type, CollisionMask mask);
// Update collision data for an object (call when object moves)
void updateCollider(uint16_t gameObjectIndex, const psyqo::Vec3& position,
const psyqo::Matrix33& rotation);
// Run collision detection for one frame
// Returns number of collisions detected
int detectCollisions();
// Get collision results (valid until next detectCollisions call)
const CollisionResult* getResults() const { return m_results; }
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; }
private:
// Collision data for all registered colliders
CollisionData m_colliders[MAX_COLLIDERS];
int m_colliderCount = 0;
// Spatial partitioning grid
SpatialGrid m_grid;
// Collision results for current frame
CollisionResult m_results[MAX_COLLISION_RESULTS];
int m_resultCount = 0;
// Trigger tracking
TriggerPair m_triggerPairs[MAX_TRIGGERS];
int m_triggerPairCount = 0;
// Narrowphase AABB test
bool testAABB(const AABB& a, const AABB& b,
psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const;
// Update trigger state machine
void updateTriggerState(uint16_t triggerIndex, uint16_t otherIndex, bool isOverlapping);
};
} // namespace psxsplash

148
src/controls.cpp Normal file
View File

@@ -0,0 +1,148 @@
#include "controls.hh"
#include <psyqo/vector.hh>
void psxsplash::Controls::Init() { m_input.initialize(); }
bool psxsplash::Controls::isDigitalPad() const {
uint8_t padType = m_input.getPadType(psyqo::AdvancedPad::Pad::Pad1a);
// Digital pad (0x41) has no analog sticks
// Also treat disconnected pads as digital (D-pad still works through button API)
return padType == psyqo::AdvancedPad::PadType::DigitalPad ||
padType == psyqo::AdvancedPad::PadType::None;
}
void psxsplash::Controls::getDpadAxes(int16_t &outX, int16_t &outY) const {
outX = 0;
outY = 0;
// D-pad produces full-magnitude values (like pushing the stick to the edge)
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Up))
outY = -127;
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Down))
outY = 127;
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Left))
outX = -127;
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Right))
outX = 127;
}
void psxsplash::Controls::UpdateButtonStates() {
m_previousButtons = m_currentButtons;
// Read all button states into a single bitmask
m_currentButtons = 0;
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Cross)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Cross);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Circle)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Circle);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Square)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Square);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Triangle)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Triangle);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::L1);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L2)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::L2);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L3)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::L3);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::R1);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R2)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::R2);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R3)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::R3);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Start)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Start);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Select)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Select);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Up)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Up);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Down)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Down);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Left)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Left);
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Right)) m_currentButtons |= (1u << psyqo::AdvancedPad::Button::Right);
// Calculate pressed and released buttons
m_buttonsPressed = m_currentButtons & ~m_previousButtons;
m_buttonsReleased = m_previousButtons & ~m_currentButtons;
}
void psxsplash::Controls::HandleControls(psyqo::Vec3 &playerPosition, psyqo::Angle &playerRotationX,
psyqo::Angle &playerRotationY, psyqo::Angle &playerRotationZ, bool freecam,
int deltaFrames) {
bool digital = isDigitalPad();
int16_t rightXOffset, rightYOffset, leftXOffset, leftYOffset;
if (digital) {
// Digital pad: use D-pad for movement, L1/R1 for rotation
getDpadAxes(leftXOffset, leftYOffset);
// L1/R1 for horizontal look rotation (no vertical on digital)
rightXOffset = 0;
rightYOffset = 0;
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1))
rightXOffset = 90;
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1))
rightXOffset = -90;
} else {
// Analog pad: read stick ADC values
uint8_t rightX = m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 0);
uint8_t rightY = m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 1);
uint8_t leftX = m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 2);
uint8_t leftY = m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 3);
rightXOffset = (int16_t)rightX - 0x80;
rightYOffset = (int16_t)rightY - 0x80;
leftXOffset = (int16_t)leftX - 0x80;
leftYOffset = (int16_t)leftY - 0x80;
// On analog pad, also check D-pad as fallback (when sticks are centered)
if (__builtin_abs(leftXOffset) < m_stickDeadzone && __builtin_abs(leftYOffset) < m_stickDeadzone) {
int16_t dpadX, dpadY;
getDpadAxes(dpadX, dpadY);
if (dpadX != 0 || dpadY != 0) {
leftXOffset = dpadX;
leftYOffset = dpadY;
}
}
}
// Sprint toggle (L3 for analog, Square for digital)
if (__builtin_abs(leftXOffset) < m_stickDeadzone && __builtin_abs(leftYOffset) < m_stickDeadzone) {
m_sprinting = false;
}
// Store final stick values for Lua API access
m_leftStickX = leftXOffset;
m_leftStickY = leftYOffset;
m_rightStickX = rightXOffset;
m_rightStickY = rightYOffset;
if (digital) {
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::Square)) {
m_sprinting = true;
}
} else {
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L3)) {
m_sprinting = true;
}
}
psyqo::FixedPoint<12> speed = m_sprinting ? m_sprintSpeed : m_moveSpeed;
// Rotation (right stick or L1/R1)
if (__builtin_abs(rightXOffset) > m_stickDeadzone) {
playerRotationY += (rightXOffset * rotSpeed * deltaFrames) >> 7;
}
if (__builtin_abs(rightYOffset) > m_stickDeadzone) {
playerRotationX -= (rightYOffset * rotSpeed * deltaFrames) >> 7;
playerRotationX = eastl::clamp(playerRotationX, -0.5_pi, 0.5_pi);
}
// Movement (left stick or D-pad)
if (__builtin_abs(leftYOffset) > m_stickDeadzone) {
psyqo::FixedPoint<12> forward = -(leftYOffset * speed * deltaFrames) >> 7;
playerPosition.x += m_trig.sin(playerRotationY) * forward;
playerPosition.z += m_trig.cos(playerRotationY) * forward;
}
if (__builtin_abs(leftXOffset) > m_stickDeadzone) {
psyqo::FixedPoint<12> strafe = -(leftXOffset * speed * deltaFrames) >> 7;
playerPosition.x -= m_trig.cos(playerRotationY) * strafe;
playerPosition.z += m_trig.sin(playerRotationY) * strafe;
}
if (freecam) {
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1)) {
playerPosition.y += speed * deltaFrames;
}
if (m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1)) {
playerPosition.y -= speed * deltaFrames;
}
}
}

86
src/controls.hh Normal file
View File

@@ -0,0 +1,86 @@
#pragma once
#include <psyqo/advancedpad.hh>
#include <psyqo/trigonometry.hh>
#include <psyqo/vector.hh>
#include <psyqo/fixed-point.hh>
namespace psxsplash {
using namespace psyqo::fixed_point_literals;
using namespace psyqo::trig_literals;
class Controls {
public:
void Init();
void HandleControls(psyqo::Vec3 &playerPosition, psyqo::Angle &playerRotationX, psyqo::Angle &playerRotationY,
psyqo::Angle &playerRotationZ, bool freecam, int deltaFrames);
/// Update button state tracking - call before HandleControls
void UpdateButtonStates();
/// Set movement speeds from splashpack data (call once after scene load)
void setMoveSpeed(psyqo::FixedPoint<12, uint16_t> speed) { m_moveSpeed.value = speed.value; }
void setSprintSpeed(psyqo::FixedPoint<12, uint16_t> speed) { m_sprintSpeed.value = speed.value; }
/// Check if a button was just pressed this frame
bool wasButtonPressed(psyqo::AdvancedPad::Button button) const {
uint16_t mask = 1u << static_cast<uint16_t>(button);
return (m_currentButtons & mask) && !(m_previousButtons & mask);
}
/// Check if a button was just released this frame
bool wasButtonReleased(psyqo::AdvancedPad::Button button) const {
uint16_t mask = 1u << static_cast<uint16_t>(button);
return !(m_currentButtons & mask) && (m_previousButtons & mask);
}
/// Check if a button is currently held
bool isButtonHeld(psyqo::AdvancedPad::Button button) const {
return m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, button);
}
/// Get bitmask of buttons pressed this frame
uint16_t getButtonsPressed() const { return m_buttonsPressed; }
/// Get bitmask of buttons released this frame
uint16_t getButtonsReleased() const { return m_buttonsReleased; }
/// Analog stick accessors (set during HandleControls)
int16_t getLeftStickX() const { return m_leftStickX; }
int16_t getLeftStickY() const { return m_leftStickY; }
int16_t getRightStickX() const { return m_rightStickX; }
int16_t getRightStickY() const { return m_rightStickY; }
private:
psyqo::AdvancedPad m_input;
psyqo::Trig<> m_trig;
bool m_sprinting = false;
static constexpr uint8_t m_stickDeadzone = 0x30;
static constexpr psyqo::Angle rotSpeed = 0.01_pi;
// Configurable movement speeds (set from splashpack, or defaults)
psyqo::FixedPoint<12> m_moveSpeed = 0.002_fp;
psyqo::FixedPoint<12> m_sprintSpeed = 0.01_fp;
// Button state tracking
uint16_t m_previousButtons = 0;
uint16_t m_currentButtons = 0;
uint16_t m_buttonsPressed = 0;
uint16_t m_buttonsReleased = 0;
// Analog stick values (centered at 0, range -127 to +127)
int16_t m_leftStickX = 0;
int16_t m_leftStickY = 0;
int16_t m_rightStickX = 0;
int16_t m_rightStickY = 0;
/// Returns true if the connected pad is digital-only (no analog sticks)
bool isDigitalPad() const;
/// Get movement axes from D-pad as simulated stick values (-127 to +127)
void getDpadAxes(int16_t &outX, int16_t &outY) const;
};
}; // namespace psxsplash

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <cstdint> #include <common/util/bitfield.hh>
#include <psyqo/matrix.hh> #include <psyqo/matrix.hh>
#include <psyqo/vector.hh> #include <psyqo/vector.hh>
@@ -8,7 +8,24 @@
namespace psxsplash { namespace psxsplash {
class Lua; // Forward declaration
// Component index constants - 0xFFFF means no component
constexpr uint16_t NO_COMPONENT = 0xFFFF;
/**
* GameObject bitfield flags
*
* Bit 0: isActive - whether object is active in scene
* Bit 1: pendingEnable - flag for deferred enable (to batch Lua calls)
* Bit 2: pendingDisable - flag for deferred disable
*/
class GameObject final { class GameObject final {
typedef Utilities::BitSpan<bool> IsActive;
typedef Utilities::BitSpan<bool, 1> PendingEnable;
typedef Utilities::BitSpan<bool, 2> PendingDisable;
typedef Utilities::BitField<IsActive, PendingEnable, PendingDisable> GameObjectFlags;
public: public:
union { union {
Tri *polygons; Tri *polygons;
@@ -16,8 +33,43 @@ class GameObject final {
}; };
psyqo::Vec3 position; psyqo::Vec3 position;
psyqo::Matrix33 rotation; psyqo::Matrix33 rotation;
// Mesh data
uint16_t polyCount; uint16_t polyCount;
uint16_t reserved; int16_t luaFileIndex;
union {
GameObjectFlags flags;
uint32_t flagsAsInt;
};
// Component indices (0xFFFF = no component)
uint16_t interactableIndex;
uint16_t _reserved0; // Was healthIndex (legacy, kept for binary layout)
// Runtime-only: Lua event bitmask (set during RegisterGameObject)
// In the splashpack binary these 4 bytes are _reserved1 + _reserved2 (zeros).
uint32_t eventMask;
// World-space AABB (20.12 fixed-point, 24 bytes)
// Used for per-object frustum culling before iterating triangles
int32_t aabbMinX, aabbMinY, aabbMinZ;
int32_t aabbMaxX, aabbMaxY, aabbMaxZ;
// Basic accessors
bool isActive() const { return flags.get<IsActive>(); }
// setActive with Lua event support - call the version that takes Lua& for events
void setActive(bool active) { flags.set<IsActive>(active); }
// Deferred enable/disable for batched Lua calls
bool isPendingEnable() const { return flags.get<PendingEnable>(); }
bool isPendingDisable() const { return flags.get<PendingDisable>(); }
void setPendingEnable(bool pending) { flags.set<PendingEnable>(pending); }
void setPendingDisable(bool pending) { flags.set<PendingDisable>(pending); }
// Component checks
bool hasInteractable() const { return interactableIndex != NO_COMPONENT; }
}; };
static_assert(sizeof(GameObject) == 56, "GameObject is not 56 bytes"); static_assert(sizeof(GameObject) == 92, "GameObject is not 92 bytes");
} // namespace psxsplash } // namespace psxsplash

View File

@@ -6,29 +6,31 @@
using namespace psyqo::GTE; using namespace psyqo::GTE;
void psxsplash::MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result) { void psxsplash::MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result) {
writeSafe<PseudoRegister::Rotation>(matA); // Load matA as the rotation matrix. No prior GTE op depends on RT registers here.
writeUnsafe<PseudoRegister::Rotation>(matA);
psyqo::Vec3 t; psyqo::Vec3 t;
// Column 0 of matB: Safe write to V0 ensures rotation matrix is settled before MVMVA.
psyqo::GTE::writeSafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].x, matB.vs[1].x, matB.vs[2].x}); psyqo::GTE::writeSafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].x, matB.vs[1].x, matB.vs[2].x});
psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>(); psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>();
// Safe read: MVMVA (8 cycles) output must be stable before reading.
t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>(); t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
result->vs[0].x = t.x; result->vs[0].x = t.x;
result->vs[1].x = t.y; result->vs[1].x = t.y;
result->vs[2].x = t.z; result->vs[2].x = t.z;
psyqo::GTE::writeSafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].y, matB.vs[1].y, matB.vs[2].y}); // Column 1: Unsafe V0 write is fine since MVMVA just completed (no dependency on V0 from readSafe).
psyqo::GTE::writeUnsafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].y, matB.vs[1].y, matB.vs[2].y});
// Safe nop-equivalent: the compiler inserts enough instructions between write and kernel call.
psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>(); psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>();
t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>(); t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
result->vs[0].y = t.x; result->vs[0].y = t.x;
result->vs[1].y = t.y; result->vs[1].y = t.y;
result->vs[2].y = t.z; result->vs[2].y = t.z;
psyqo::GTE::writeSafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].z, matB.vs[1].z, matB.vs[2].z}); // Column 2: Same pattern.
psyqo::GTE::writeUnsafe<PseudoRegister::V0>(psyqo::Vec3{matB.vs[0].z, matB.vs[1].z, matB.vs[2].z});
psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>(); psyqo::GTE::Kernels::mvmva<Kernels::MX::RT, Kernels::MV::V0>();
t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>(); t = psyqo::GTE::readSafe<psyqo::GTE::PseudoRegister::SV>();
result->vs[0].z = t.x; result->vs[0].z = t.x;

View File

@@ -1,4 +1,5 @@
#pragma once #pragma once
#include <psyqo/matrix.hh> #include <psyqo/matrix.hh>
namespace psxsplash { namespace psxsplash {

54
src/interactable.hh Normal file
View File

@@ -0,0 +1,54 @@
#pragma once
#include <stdint.h>
#include <psyqo/fixed-point.hh>
namespace psxsplash {
/**
* Interactable component - enables player interaction with objects.
*
* When the player is within interaction radius and presses the interact button,
* the onInteract Lua event fires on the associated GameObject.
*/
struct Interactable {
// Interaction radius squared (fixed-point 12-bit, pre-squared for fast distance checks)
psyqo::FixedPoint<12> radiusSquared;
// Interaction point offset from object center
psyqo::FixedPoint<12> offsetX;
psyqo::FixedPoint<12> offsetY;
psyqo::FixedPoint<12> offsetZ;
// Button index that triggers interaction (0-15)
uint8_t interactButton;
// Configuration flags
uint8_t flags; // bit 0: isRepeatable, bit 1: showPrompt, bit 2: requireLineOfSight
// Cooldown between interactions (in frames)
uint16_t cooldownFrames;
// Runtime state
uint16_t currentCooldown; // Frames remaining until can interact again
uint16_t gameObjectIndex; // Index of associated GameObject
// Flag accessors
bool isRepeatable() const { return flags & 0x01; }
bool showPrompt() const { return flags & 0x02; }
bool requireLineOfSight() const { return flags & 0x04; }
// Check if ready to interact
bool canInteract() const { return currentCooldown == 0; }
// Called when interaction happens
void triggerCooldown() { currentCooldown = cooldownFrames; }
// Called each frame to decrement cooldown
void update() {
if (currentCooldown > 0) currentCooldown--;
}
};
static_assert(sizeof(Interactable) == 24, "Interactable is not 24 bytes");
} // namespace psxsplash

View File

@@ -1,186 +1,443 @@
#include "lua.h" #include "lua.h"
#include <psyqo-lua/lua.hh>
#include <psyqo/soft-math.hh>
#include <psyqo/trigonometry.hh>
#include <psyqo/xprintf.h> #include <psyqo/xprintf.h>
#include "gameobject.hh" #include "gameobject.hh"
#include "psyqo-lua/lua.hh"
constexpr const char METATABLE_SCRIPT[] = R"( constexpr const char GAMEOBJECT_SCRIPT[] = R"(
print("test") return function(metatable)
metatableForAllGameObjects = { local get_position = metatable.get_position
__index = function(self, key) local set_position = metatable.set_position
if key == "position" then local get_active = metatable.get_active
local pos = rawget(self, key) local set_active = metatable.set_active
if pos == nil then local get_rotationY = metatable.get_rotationY
pos = get_position(self.__cpp_ptr) local set_rotationY = metatable.set_rotationY
rawset(self, key, pos)
end
return pos
end
return rawget(self, key)
end,
__newindex = function(self, key, value) metatable.get_position = nil
metatable.set_position = nil
metatable.get_active = nil
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 if key == "position" then
-- Option 1: Directly update C++
set_position(self.__cpp_ptr, value) set_position(self.__cpp_ptr, value)
-- Option 2: Also update local cache: return
rawset(self, key, value) elseif key == "active" then
set_active(self.__cpp_ptr, value)
return
elseif key == "rotationY" then
set_rotationY(self.__cpp_ptr, value)
return return
end end
rawset(self, key, value) rawset(self, key, value)
end end
} end
)"; )";
// Lua helpers // Lua helpers
int luaPrint(psyqo::Lua L) { static constexpr lua_Number kFixedScale = 4096;
int n = L.getTop(); // Get the number of arguments
for (int i = 1; i <= n; i++) { static int gameobjectGetPosition(psyqo::Lua L) {
if (i > 1) {
printf("\t"); // Tab between arguments
}
// Check the type of the argument auto go = L.toUserdata<psxsplash::GameObject>(1);
if (L.isString(i)) {
printf("%s", L.toString(i)); // If it's a string, print it L.newTable();
} else if (L.isNumber(i)) { L.pushNumber(static_cast<lua_Number>(go->position.x.raw()) / kFixedScale);
printf("%g", L.toNumber(i)); // If it's a number, print it L.setField(2, "x");
} else { L.pushNumber(static_cast<lua_Number>(go->position.y.raw()) / kFixedScale);
// For other types, just print their type (you can expand this if needed) L.setField(2, "y");
printf("[%s]", L.typeName(i)); L.pushNumber(static_cast<lua_Number>(go->position.z.raw()) / kFixedScale);
} L.setField(2, "z");
}
printf("\n"); return 1;
return 0; // No return value
} }
static int gameobjectSetPosition(lua_State* L) { static int gameobjectSetPosition(psyqo::Lua L) {
psxsplash::GameObject* go = (psxsplash::GameObject*)lua_touserdata(L, 1);
lua_newtable(L); auto go = L.toUserdata<psxsplash::GameObject>(1);
lua_pushnumber(L, go->position.x.raw());
lua_setfield(L, -2, "x"); L.getField(2, "x");
lua_pushnumber(L, go->position.y.raw()); go->position.x = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW);
lua_setfield(L, -2, "y"); L.pop();
lua_pushnumber(L, go->position.z.raw());
lua_setfield(L, -2, "z"); L.getField(2, "y");
go->position.y = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW);
L.pop();
L.getField(2, "z");
go->position.z = psyqo::FixedPoint<>(static_cast<int32_t>(L.toNumber(3) * kFixedScale), psyqo::FixedPoint<>::RAW);
L.pop();
return 0;
}
static int gamobjectGetActive(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1);
L.push(go->isActive());
return 1; return 1;
} }
static int gameobjectGetPosition(lua_State* L) { static int gamobjectSetActive(psyqo::Lua L) {
psxsplash::GameObject* go = (psxsplash::GameObject*)lua_touserdata(L, 1); auto go = L.toUserdata<psxsplash::GameObject>(1);
bool active = L.toBoolean(2);
go->setActive(active);
return 0;
}
lua_getfield(L, 2, "x"); // Angle constants: psyqo::Angle is FixedPoint<10>, so 1.0_pi = raw 1024
psyqo::FixedPoint<> x(lua_tonumber(L, -1), psyqo::FixedPoint<>::RAW); static constexpr lua_Number kAngleScale = 1024;
go->position.x = x; static psyqo::Trig<> s_trig;
lua_pop(L, 1);
lua_getfield(L, 2, "y");
psyqo::FixedPoint<> y(lua_tonumber(L, -1), psyqo::FixedPoint<>::RAW);
go->position.x = x;
lua_pop(L, 1);
lua_getfield(L, 2, "z");
psyqo::FixedPoint<> z(lua_tonumber(L, -1), psyqo::FixedPoint<>::RAW);
go->position.x = x;
lua_pop(L, 1);
// 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; }
int32_t abs_s = sinVal < 0 ? -sinVal : sinVal;
int32_t abs_c = cosVal < 0 ? -cosVal : cosVal;
int32_t minV = abs_s < abs_c ? abs_s : abs_c;
int32_t maxV = abs_s > abs_c ? abs_s : abs_c;
// 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 (sinVal < 0) angle = -angle;
result.value = angle;
return result;
}
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;
}
static int gameobjectSetRotationY(psyqo::Lua L) {
auto go = L.toUserdata<psxsplash::GameObject>(1);
lua_Number piUnits = L.toNumber(2);
psyqo::Angle angle;
angle.value = static_cast<int32_t>(piUnits * kAngleScale);
go->rotation = psyqo::SoftMath::generateRotationMatrix33(angle, psyqo::SoftMath::Axis::Y, s_trig);
return 0; return 0;
} }
void psxsplash::Lua::Init() { void psxsplash::Lua::Init() {
L.push(luaPrint); auto L = m_state;
L.setGlobal("print"); // Load and run the game objects script
if (L.loadBuffer(GAMEOBJECT_SCRIPT, "buffer:gameObjects") == 0) {
if (L.pcall(0, 1) == 0) {
// This will be our metatable
L.newTable();
L.push(gameobjectGetPosition); L.push(gameobjectGetPosition);
L.setGlobal("get_position"); L.setField(-2, "get_position");
L.push(gameobjectSetPosition); L.push(gameobjectSetPosition);
L.setGlobal("set_position"); L.setField(-2, "set_position");
// Load and run the metatable script L.push(gamobjectGetActive);
if (L.loadBuffer(METATABLE_SCRIPT, "metatableForAllGameObjects") == 0) { L.setField(-2, "get_active");
if (L.pcall(0, 0) == 0) {
// Script executed successfully L.push(gamobjectSetActive);
printf("Lua script 'metatableForAllGameObjects' loaded successfully\n"); L.setField(-2, "set_active");
L.push(gameobjectGetRotationY);
L.setField(-2, "get_rotationY");
L.push(gameobjectSetRotationY);
L.setField(-2, "set_rotationY");
L.copy(-1);
m_metatableReference = L.ref();
if (L.pcall(1, 0) == 0) {
// success
} else {
printf("Error registering Lua script: %s\n", L.optString(-1, "Unknown error"));
L.clearStack();
return;
}
} else { } else {
// Print Lua error if script execution fails // Print Lua error if script execution fails
printf("Error executing Lua script: %s\n", L.isString(-1) ? L.toString(-1) : "Unknown error"); printf("Error executing Lua script: %s\n", L.optString(-1, "Unknown error"));
L.pop(); L.clearStack();
return;
} }
} else { } else {
// Print Lua error if script loading fails // Print Lua error if script loading fails
printf("Error loading Lua script: %s\n", L.isString(-1) ? L.toString(-1) : "Unknown error"); printf("Error loading Lua script: %s\n", L.optString(-1, "Unknown error"));
L.pop(); L.clearStack();
return;
} }
// Check if the metatable was set as a global L.newTable();
L.getGlobal("metatableForAllGameObjects"); m_luascriptsReference = L.ref();
if (L.isTable(-1)) {
printf("metatableForAllGameObjects successfully set as a global\n");
} else {
printf("Warning: metatableForAllGameObjects not found after init\n");
}
L.pop(); // Pop the global check
} }
void psxsplash::Lua::LoadLuaFile(const char* code, size_t len) { void psxsplash::Lua::Shutdown() {
if (L.loadBuffer(code, len) != LUA_OK) { // 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();
}
m_metatableReference = LUA_NOREF;
m_luascriptsReference = LUA_NOREF;
m_luaSceneScriptsReference = LUA_NOREF;
}
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)
Init();
}
void psxsplash::Lua::LoadLuaFile(const char* code, size_t len, int index) {
auto L = m_state;
char filename[32];
snprintf(filename, sizeof(filename), "lua_asset:%d", index);
if (L.loadBuffer(code, len, filename) != LUA_OK) {
printf("Lua error: %s\n", L.toString(-1)); printf("Lua error: %s\n", L.toString(-1));
L.pop(); L.pop();
return;
} }
// (1) script func
L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference);
// (1) script func (2) scripts table
L.newTable();
// (1) script func (2) scripts table (3) env {}
// Give the environment a metatable that falls back to _G
// so scripts can see Entity, Debug, Input, etc.
L.newTable();
// (1) script func (2) scripts table (3) env {} (4) mt {}
L.pushGlobalTable();
// (1) script func (2) scripts table (3) env {} (4) mt {} (5) _G
L.setField(-2, "__index");
// (1) script func (2) scripts table (3) env {} (4) mt { __index = _G }
L.setMetatable(-2);
// (1) script func (2) scripts table (3) env { mt }
L.pushNumber(index);
// (1) script func (2) scripts table (3) env (4) index
L.copy(-2);
// (1) script func (2) scripts table (3) env (4) index (5) env
L.setTable(-4);
// (1) script func (2) scripts table (3) env
lua_setupvalue(L.getState(), -3, 1);
// (1) script func (2) scripts table
L.pop();
// (1) script func
if (L.pcall(0, 0)) { if (L.pcall(0, 0)) {
printf("Lua error: %s\n", L.toString(-1)); printf("Lua error: %s\n", L.toString(-1));
L.pop(); L.pop();
} }
} }
void psxsplash::Lua::RegisterGameObject(GameObject* go) { void psxsplash::Lua::RegisterSceneScripts(int index) {
// Create a new Lua table for the GameObject if (index < 0) return;
auto L = m_state;
L.newTable(); L.newTable();
// (1) {}
// Set the __cpp_ptr field to store the C++ pointer L.copy(1);
L.push(go); // (1) {} (2) {}
L.setField(-2, "__cpp_ptr"); m_luaSceneScriptsReference = L.ref();
// (1) {}
// Set the metatable for the table L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference);
L.getGlobal("metatableForAllGameObjects"); // (1) {} (2) scripts table
if (L.isTable(-1)) { L.pushNumber(index);
L.setMetatable(-2); // Set the metatable for the table // (1) {} (2) script environments table (3) index
} else { L.getTable(-2);
printf("Warning: metatableForAllGameObjects not found\n"); // (1) {} (2) script environments table (3) script environment table for the scene
L.pop(); // Pop the invalid metatable if (!L.isTable(-1)) {
} // Scene Lua file index is invalid or script not loaded
printf("Warning: scene Lua file index %d not found\n", index);
L.push(go); L.pop(3);
L.push(-2);
L.rawSet(LUA_REGISTRYINDEX);
// Debugging: Confirm the GameObject was registered
printf("GameObject registered in Lua registry: %p\n", go);
L.pop();
}
void psxsplash::Lua::CallOnCollide(GameObject* self, GameObject* other) {
L.getGlobal("onCollision");
if (!L.isFunction(-1)) {
printf("Lua function 'onCollide' not found\n");
L.pop();
return; return;
} }
onSceneCreationStartFunctionWrapper.resolveGlobal(L);
onSceneCreationEndFunctionWrapper.resolveGlobal(L);
L.pop(3);
// empty stack
}
PushGameObject(self); // We're going to store the Lua table for the object at the address of the object,
PushGameObject(other); // and the table for its methods at the address of the object + 1 byte.
void psxsplash::Lua::RegisterGameObject(GameObject* go) {
if (L.pcall(2, 0)) { uint8_t* ptr = reinterpret_cast<uint8_t*>(go);
printf("Lua error: %s\n", L.toString(-1)); auto L = m_state;
L.push(ptr);
// (1) go
L.newTable();
// (1) go (2) {}
L.push(ptr);
// (1) go (2) {} (3) go
L.setField(-2, "__cpp_ptr");
// (1) go (2) { __cpp_ptr = go }
L.rawGetI(LUA_REGISTRYINDEX, m_metatableReference);
// (1) go (2) { __cpp_ptr = go } (3) metatable
if (L.isTable(-1)) {
L.setMetatable(-2);
} else {
printf("Warning: metatableForAllGameObjects not found\n");
L.pop(); L.pop();
} }
// (1) go (2) { __cpp_ptr = go + metatable }
L.rawSet(LUA_REGISTRYINDEX);
// empty stack
L.newTable();
// (1) {}
L.push(ptr + 1);
// (1) {} (2) go + 1
L.copy(1);
// (1) {} (2) go + 1 (3) {}
L.rawSet(LUA_REGISTRYINDEX);
// (1) {}
// Initialize event mask for this object
uint32_t eventMask = EVENT_NONE;
if (go->luaFileIndex != -1) {
L.rawGetI(LUA_REGISTRYINDEX, m_luascriptsReference);
// (1) {} (2) script environments table
L.rawGetI(-1, go->luaFileIndex);
// (1) {} (2) script environments table (3) script environment table for this object
// Guard: if the script file failed to load (e.g. compilation error),
// the environment will be nil — skip event resolution.
if (!L.isTable(-1)) {
L.pop(2);
} else {
// Resolve each event and build the bitmask
// 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
go->eventMask = eventMask;
L.pop();
// empty stack
// Fire onCreate event if this object handles it
if (eventMask & EVENT_ON_CREATE) {
onCreateMethodWrapper.callMethod(*this, go);
}
}
void psxsplash::Lua::OnCollision(GameObject* self, GameObject* other) {
if (!hasEvent(self, EVENT_ON_COLLISION)) return;
onCollisionMethodWrapper.callMethod(*this, self, other);
}
void psxsplash::Lua::OnInteract(GameObject* self) {
if (!hasEvent(self, EVENT_ON_INTERACT)) return;
onInteractMethodWrapper.callMethod(*this, self);
}
void psxsplash::Lua::OnTriggerEnter(GameObject* trigger, GameObject* other) {
if (!hasEvent(trigger, EVENT_ON_TRIGGER_ENTER)) return;
onTriggerEnterMethodWrapper.callMethod(*this, trigger, other);
}
void psxsplash::Lua::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) {
if (!hasEvent(trigger, EVENT_ON_TRIGGER_EXIT)) return;
onTriggerExitMethodWrapper.callMethod(*this, trigger, other);
}
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;
}
void psxsplash::Lua::OnEnable(GameObject* go) {
if (!hasEvent(go, EVENT_ON_ENABLE)) return;
onEnableMethodWrapper.callMethod(*this, go);
}
void psxsplash::Lua::OnDisable(GameObject* go) {
if (!hasEvent(go, EVENT_ON_DISABLE)) return;
onDisableMethodWrapper.callMethod(*this, go);
}
void psxsplash::Lua::OnButtonPress(GameObject* go, int button) {
if (!hasEvent(go, EVENT_ON_BUTTON_PRESS)) return;
onButtonPressMethodWrapper.callMethod(*this, go, button);
}
void psxsplash::Lua::OnButtonRelease(GameObject* go, int button) {
if (!hasEvent(go, EVENT_ON_BUTTON_RELEASE)) return;
onButtonReleaseMethodWrapper.callMethod(*this, go, button);
}
void psxsplash::Lua::OnUpdate(GameObject* go, int deltaFrames) {
if (!hasEvent(go, EVENT_ON_UPDATE)) return;
onUpdateMethodWrapper.callMethod(*this, go, deltaFrames);
} }
void psxsplash::Lua::PushGameObject(GameObject* go) { void psxsplash::Lua::PushGameObject(GameObject* go) {
auto L = m_state;
L.push(go); L.push(go);
L.rawGet(LUA_REGISTRYINDEX); L.rawGet(LUA_REGISTRYINDEX);

176
src/lua.h
View File

@@ -1,20 +1,188 @@
#pragma once #pragma once
#include <psyqo-lua/lua.hh> #include <stdint.h>
#include "gameobject.hh" #include "gameobject.hh"
#include "psyqo-lua/lua.hh"
#include "psyqo/xprintf.h"
#include "typestring.h"
namespace psxsplash { namespace psxsplash {
struct LuaFile {
union {
uint32_t luaCodeOffset;
const char* luaCode;
};
uint32_t length;
};
/**
* Event bitmask flags - each bit represents whether an object handles that event.
* This allows O(1) checking before calling into Lua VM.
*
* CRITICAL: The PS1 cannot afford to call into Lua for events objects don't handle.
* When registering a GameObject, we scan its script and set these bits.
* During dispatch, we check the bit FIRST before any Lua VM access.
*/
enum EventMask : uint32_t {
EVENT_NONE = 0,
EVENT_ON_CREATE = 1 << 0,
EVENT_ON_COLLISION = 1 << 1,
EVENT_ON_INTERACT = 1 << 2,
EVENT_ON_TRIGGER_ENTER = 1 << 3,
EVENT_ON_TRIGGER_STAY = 1 << 4,
EVENT_ON_TRIGGER_EXIT = 1 << 5,
EVENT_ON_UPDATE = 1 << 6,
EVENT_ON_DESTROY = 1 << 7,
EVENT_ON_ENABLE = 1 << 8,
EVENT_ON_DISABLE = 1 << 9,
EVENT_ON_BUTTON_PRESS = 1 << 10,
EVENT_ON_BUTTON_RELEASE = 1 << 11,
};
class Lua { class Lua {
public: public:
void Init(); void Init();
void Reset(); // Destroy and recreate the Lua VM (call on scene load)
void Shutdown(); // Close the Lua VM without recreating (call on scene unload)
void LoadLuaFile(const char* code, size_t len); void LoadLuaFile(const char* code, size_t len, int index);
void RegisterSceneScripts(int index);
void RegisterGameObject(GameObject* go); void RegisterGameObject(GameObject* go);
void CallOnCollide(GameObject* self, GameObject* other);
// Get the underlying psyqo::Lua state for API registration
psyqo::Lua& getState() { return m_state; }
/**
* Check if a GameObject handles a specific event.
* Call this BEFORE attempting to dispatch any event.
*/
bool hasEvent(GameObject* go, EventMask event) const {
return (go->eventMask & event) != 0;
}
void OnSceneCreationStart() {
onSceneCreationStartFunctionWrapper.callFunction(*this);
}
void OnSceneCreationEnd() {
onSceneCreationEndFunctionWrapper.callFunction(*this);
}
// Event dispatchers - these check the bitmask before calling Lua
void OnCollision(GameObject* self, GameObject* other);
void OnInteract(GameObject* self);
void OnTriggerEnter(GameObject* trigger, GameObject* other);
void OnTriggerStay(GameObject* trigger, GameObject* other);
void OnTriggerExit(GameObject* trigger, GameObject* other);
void OnUpdate(GameObject* go, int deltaFrames); // Per-object update
void OnDestroy(GameObject* go);
void OnEnable(GameObject* go);
void OnDisable(GameObject* go);
void OnButtonPress(GameObject* go, int button);
void OnButtonRelease(GameObject* go, int button);
private: private:
template <int methodId, typename methodName>
struct FunctionWrapper;
template <int methodId, char... C>
struct FunctionWrapper<methodId, irqus::typestring<C...>> {
typedef irqus::typestring<C...> methodName;
// Returns true if the function was found and stored
static bool resolveGlobal(psyqo::Lua L) {
L.push(methodName::data(), methodName::size());
L.getTable(3);
if (L.isFunction(-1)) {
L.pushNumber(methodId);
L.copy(-2);
L.setTable(1);
L.pop(); // Pop the function
return true;
} else {
L.pop();
return false;
}
}
template <typename... Args>
static void pushArgs(psxsplash::Lua& lua, Args... args) {
(push(lua, args), ...);
}
static void push(psxsplash::Lua& lua, GameObject* go) { lua.PushGameObject(go); }
static void push(psxsplash::Lua& lua, int val) { lua.m_state.pushNumber(val); }
template <typename... Args>
static void callMethod(psxsplash::Lua& lua, GameObject* go, Args... args) {
auto L = lua.m_state;
uint8_t* ptr = reinterpret_cast<uint8_t*>(go);
L.push(ptr + 1);
L.rawGet(LUA_REGISTRYINDEX);
L.rawGetI(-1, methodId);
if (!L.isFunction(-1)) {
L.clearStack();
return;
}
lua.PushGameObject(go);
pushArgs(lua, args...);
if (L.pcall(sizeof...(Args) + 1, 0) != LUA_OK) {
printf("Lua error: %s\n", L.toString(-1));
}
L.clearStack();
}
template <typename... Args>
static void callFunction(psxsplash::Lua& lua, Args... args) {
auto L = lua.m_state;
L.rawGetI(LUA_REGISTRYINDEX, lua.m_luaSceneScriptsReference);
if (!L.isTable(-1)) {
L.clearStack();
return;
}
L.rawGetI(-1, methodId);
if (!L.isFunction(-1)) {
L.clearStack();
return;
}
pushArgs(lua, args...);
if (L.pcall(sizeof...(Args), 0) != LUA_OK) {
printf("Lua error: %s\n", L.toString(-1));
}
L.clearStack();
}
};
// Scene-level events (methodId 1-2)
[[no_unique_address]] FunctionWrapper<1, typestring_is("onSceneCreationStart")> onSceneCreationStartFunctionWrapper;
[[no_unique_address]] FunctionWrapper<2, typestring_is("onSceneCreationEnd")> onSceneCreationEndFunctionWrapper;
// Object-level events (methodId 100-111, offset to avoid collision with scene events)
[[no_unique_address]] FunctionWrapper<100, typestring_is("onCreate")> onCreateMethodWrapper;
[[no_unique_address]] FunctionWrapper<101, typestring_is("onCollision")> onCollisionMethodWrapper;
[[no_unique_address]] FunctionWrapper<102, typestring_is("onInteract")> onInteractMethodWrapper;
[[no_unique_address]] FunctionWrapper<103, typestring_is("onTriggerEnter")> onTriggerEnterMethodWrapper;
[[no_unique_address]] FunctionWrapper<104, typestring_is("onTriggerStay")> onTriggerStayMethodWrapper;
[[no_unique_address]] FunctionWrapper<105, typestring_is("onTriggerExit")> onTriggerExitMethodWrapper;
[[no_unique_address]] FunctionWrapper<106, typestring_is("onUpdate")> onUpdateMethodWrapper;
[[no_unique_address]] FunctionWrapper<107, typestring_is("onDestroy")> onDestroyMethodWrapper;
[[no_unique_address]] FunctionWrapper<108, typestring_is("onEnable")> onEnableMethodWrapper;
[[no_unique_address]] FunctionWrapper<109, typestring_is("onDisable")> onDisableMethodWrapper;
[[no_unique_address]] FunctionWrapper<110, typestring_is("onButtonPress")> onButtonPressMethodWrapper;
[[no_unique_address]] FunctionWrapper<111, typestring_is("onButtonRelease")> onButtonReleaseMethodWrapper;
void PushGameObject(GameObject* go); void PushGameObject(GameObject* go);
psyqo::Lua L;
private:
psyqo::Lua m_state;
int m_metatableReference = LUA_NOREF;
int m_luascriptsReference = LUA_NOREF;
int m_luaSceneScriptsReference = LUA_NOREF;
// Event mask now stored inline in GameObject::eventMask
template <int methodId, typename methodName>
friend struct FunctionWrapper;
}; };
} // namespace psxsplash } // namespace psxsplash

1385
src/luaapi.cpp Normal file

File diff suppressed because it is too large Load Diff

268
src/luaapi.hh Normal file
View File

@@ -0,0 +1,268 @@
#pragma once
#include <psyqo-lua/lua.hh>
#include <psyqo/fixed-point.hh>
#include <psyqo/vector.hh>
namespace psxsplash {
class SceneManager; // Forward declaration
/**
* Lua API - Provides game scripting functionality
*
* Available namespaces:
* - Entity: Object finding, spawning, destruction
* - Vec3: Vector math operations
* - Input: Controller state queries
* - Timer: Timer control
* - Camera: Camera manipulation
* - Audio: Sound playback (future)
* - Scene: Scene management
*/
class LuaAPI {
public:
// Initialize all API modules
static void RegisterAll(psyqo::Lua& L, SceneManager* scene);
// Called once per frame to advance the Lua frame counter
static void IncrementFrameCount();
// Reset frame counter (called on scene load)
static void ResetFrameCount();
private:
// Store scene manager for API access
static SceneManager* s_sceneManager;
// ========================================================================
// ENTITY API
// ========================================================================
// Entity.FindByScriptIndex(index) -> object or nil
// Finds first object with matching Lua script file index
static int Entity_FindByScriptIndex(lua_State* L);
// Entity.FindByIndex(index) -> object or nil
// Gets object by its array index
static int Entity_FindByIndex(lua_State* L);
// Entity.Find(name) -> object or nil
// Finds first object with matching name (user-friendly)
static int Entity_Find(lua_State* L);
// Entity.GetCount() -> number
// Returns total number of game objects
static int Entity_GetCount(lua_State* L);
// Entity.SetActive(object, active)
// Sets object active state (fires onEnable/onDisable)
static int Entity_SetActive(lua_State* L);
// Entity.IsActive(object) -> boolean
static int Entity_IsActive(lua_State* L);
// Entity.GetPosition(object) -> {x, y, z}
static int Entity_GetPosition(lua_State* L);
// Entity.SetPosition(object, {x, y, z})
static int Entity_SetPosition(lua_State* L);
// Entity.GetRotationY(object) -> number (radians)
static int Entity_GetRotationY(lua_State* L);
// Entity.SetRotationY(object, angle) -> nil
static int Entity_SetRotationY(lua_State* L);
// Entity.ForEach(callback) -> nil
// Calls callback(object, index) for each active game object
static int Entity_ForEach(lua_State* L);
// ========================================================================
// VEC3 API - Vector math
// ========================================================================
// Vec3.new(x, y, z) -> {x, y, z}
static int Vec3_New(lua_State* L);
// Vec3.add(a, b) -> {x, y, z}
static int Vec3_Add(lua_State* L);
// Vec3.sub(a, b) -> {x, y, z}
static int Vec3_Sub(lua_State* L);
// Vec3.mul(v, scalar) -> {x, y, z}
static int Vec3_Mul(lua_State* L);
// Vec3.dot(a, b) -> number
static int Vec3_Dot(lua_State* L);
// Vec3.cross(a, b) -> {x, y, z}
static int Vec3_Cross(lua_State* L);
// Vec3.length(v) -> number
static int Vec3_Length(lua_State* L);
// Vec3.lengthSq(v) -> number (faster, no sqrt)
static int Vec3_LengthSq(lua_State* L);
// Vec3.normalize(v) -> {x, y, z}
static int Vec3_Normalize(lua_State* L);
// Vec3.distance(a, b) -> number
static int Vec3_Distance(lua_State* L);
// Vec3.distanceSq(a, b) -> number (faster)
static int Vec3_DistanceSq(lua_State* L);
// Vec3.lerp(a, b, t) -> {x, y, z}
static int Vec3_Lerp(lua_State* L);
// ========================================================================
// INPUT API - Controller state
// ========================================================================
// Input.IsPressed(button) -> boolean
// True only on the frame the button was pressed
static int Input_IsPressed(lua_State* L);
// Input.IsReleased(button) -> boolean
// True only on the frame the button was released
static int Input_IsReleased(lua_State* L);
// Input.IsHeld(button) -> boolean
// True while the button is held down
static int Input_IsHeld(lua_State* L);
// Input.GetAnalog(stick) -> x, y
// Returns analog stick values (-128 to 127)
static int Input_GetAnalog(lua_State* L);
// Button constants (registered as Input.CROSS, Input.CIRCLE, etc.)
static void RegisterInputConstants(psyqo::Lua& L);
// ========================================================================
// TIMER API - Frame counter
// ========================================================================
// Timer.GetFrameCount() -> number
// Returns total frames since scene start
static int Timer_GetFrameCount(lua_State* L);
// ========================================================================
// CAMERA API - Camera control
// ========================================================================
// Camera.GetPosition() -> {x, y, z}
static int Camera_GetPosition(lua_State* L);
// Camera.SetPosition(x, y, z)
static int Camera_SetPosition(lua_State* L);
// Camera.GetRotation() -> {x, y, z}
static int Camera_GetRotation(lua_State* L);
// Camera.SetRotation(x, y, z)
static int Camera_SetRotation(lua_State* L);
// Camera.LookAt(target) or Camera.LookAt(x, y, z)
static int Camera_LookAt(lua_State* L);
// ========================================================================
// AUDIO API - Sound playback (placeholder for SPU)
// ========================================================================
// Audio.Play(soundId, volume, pan) -> channelId
// soundId can be a number (clip index) or string (clip name)
static int Audio_Play(lua_State* L);
// Audio.Find(name) -> clipIndex or nil
// Finds audio clip by name, returns its index for use with Play/Stop/etc.
static int Audio_Find(lua_State* L);
// Audio.Stop(channelId)
static int Audio_Stop(lua_State* L);
// Audio.SetVolume(channelId, volume)
static int Audio_SetVolume(lua_State* L);
// Audio.StopAll()
static int Audio_StopAll(lua_State* L);
// ========================================================================
// DEBUG API - Development helpers
// ========================================================================
// Debug.Log(message)
static int Debug_Log(lua_State* L);
// Debug.DrawLine(start, end, color) - draws debug line next frame
static int Debug_DrawLine(lua_State* L);
// Debug.DrawBox(center, size, color)
static int Debug_DrawBox(lua_State* L);
// ========================================================================
// MATH API - Additional math functions
// ========================================================================
// Math.Clamp(value, min, max)
static int Math_Clamp(lua_State* L);
// Math.Lerp(a, b, t)
static int Math_Lerp(lua_State* L);
// Math.Sign(value)
static int Math_Sign(lua_State* L);
// Math.Abs(value)
static int Math_Abs(lua_State* L);
// Math.Min(a, b)
static int Math_Min(lua_State* L);
// Math.Max(a, b)
static int Math_Max(lua_State* L);
// ========================================================================
// SCENE API - Scene management
// ========================================================================
// Scene.Load(sceneIndex)
// Requests a scene transition to the given index (0-based).
// The actual load happens at the end of the current frame.
static int Scene_Load(lua_State* L);
// Scene.GetIndex() -> number
// Returns the index of the currently loaded scene.
static int Scene_GetIndex(lua_State* L);
// ========================================================================
// PERSIST API - Data that survives scene loads
// ========================================================================
// Persist.Get(key) -> number or nil
static int Persist_Get(lua_State* L);
// Persist.Set(key, value)
static int Persist_Set(lua_State* L);
// Reset all persistent data
static void PersistClear();
// ========================================================================
// HELPERS
// ========================================================================
// Push a Vec3 table onto the stack
static void PushVec3(psyqo::Lua& L, psyqo::FixedPoint<12> x,
psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z);
// Read a Vec3 table from the stack
static void ReadVec3(psyqo::Lua& L, int idx,
psyqo::FixedPoint<12>& x,
psyqo::FixedPoint<12>& y,
psyqo::FixedPoint<12>& z);
};
} // namespace psxsplash

View File

@@ -1,7 +1,3 @@
#include <stdint.h>
#include <cmath>
#include <cstdint>
#include <psyqo/advancedpad.hh> #include <psyqo/advancedpad.hh>
#include <psyqo/application.hh> #include <psyqo/application.hh>
#include <psyqo/fixed-point.hh> #include <psyqo/fixed-point.hh>
@@ -10,56 +6,31 @@
#include <psyqo/scene.hh> #include <psyqo/scene.hh>
#include <psyqo/trigonometry.hh> #include <psyqo/trigonometry.hh>
#include "EASTL/algorithm.h"
#include "camera.hh"
#include "lua.h"
#include "navmesh.hh"
#include "psyqo/vector.hh"
#include "renderer.hh" #include "renderer.hh"
#include "splashpack.hh" #include "scenemanager.hh"
#include "sceneloader.hh"
// Data from the splashpack #include "pcdrv_handler.hh"
extern uint8_t _binary_output_bin_start[];
namespace { namespace {
using namespace psyqo::fixed_point_literals;
using namespace psyqo::trig_literals;
class PSXSplash final : public psyqo::Application { class PSXSplash final : public psyqo::Application {
void prepare() override; void prepare() override;
void createScene() override; void createScene() override;
public: public:
psxsplash::Lua m_lua;
psxsplash::SplashPackLoader m_loader;
psyqo::Font<> m_font; psyqo::Font<> m_font;
psyqo::AdvancedPad m_input;
static constexpr uint8_t m_stickDeadzone = 0x30;
}; };
class MainScene final : public psyqo::Scene { class MainScene final : public psyqo::Scene {
void frame() override; void frame() override;
void start(StartReason reason) override; void start(StartReason reason) override;
psxsplash::Camera m_mainCamera;
psyqo::Angle camRotX, camRotY, camRotZ;
psyqo::Trig<> m_trig;
uint32_t m_lastFrameCounter; uint32_t m_lastFrameCounter;
static constexpr psyqo::FixedPoint<12> moveSpeed = 0.002_fp; psxsplash::SceneManager m_sceneManager;
static constexpr psyqo::Angle rotSpeed = 0.01_pi;
bool m_sprinting = false; // PCdrv-loaded scene data (owned)
static constexpr psyqo::FixedPoint<12> sprintSpeed = 0.01_fp; uint8_t* m_sceneData = nullptr;
bool m_freecam = false;
psyqo::FixedPoint<12> pheight = 0.0_fp;
bool m_renderSelect = false;
}; };
PSXSplash app; PSXSplash app;
@@ -77,42 +48,33 @@ void PSXSplash::prepare() {
// Initialize the Renderer singleton // Initialize the Renderer singleton
psxsplash::Renderer::Init(gpu()); psxsplash::Renderer::Init(gpu());
m_lua.Init();
} }
void PSXSplash::createScene() { void PSXSplash::createScene() {
m_font.uploadSystemFont(gpu()); m_font.uploadSystemFont(gpu());
m_input.initialize();
pushScene(&mainScene); pushScene(&mainScene);
} }
void MainScene::start(StartReason reason) { void MainScene::start(StartReason reason) {
app.m_loader.LoadSplashpack(_binary_output_bin_start, app.m_lua); // On real hardware: register break handler for PCDRV over SIO1 + redirect printf
app.m_lua.CallOnCollide(app.m_loader.gameObjects[0], app.m_loader.gameObjects[1]); // On emulator: no-op (pcsx-redux handles PCDRV natively)
psxsplash::Renderer::GetInstance().SetCamera(m_mainCamera); psxsplash::pcdrv_sio1_init();
m_mainCamera.SetPosition(static_cast<psyqo::FixedPoint<12>>(app.m_loader.playerStartPos.x), // Initialize PCdrv (break instructions - handled by emulator or our break handler)
static_cast<psyqo::FixedPoint<12>>(app.m_loader.playerStartPos.y), psxsplash::SceneLoader::Init();
static_cast<psyqo::FixedPoint<12>>(app.m_loader.playerStartPos.z));
pheight = psyqo::FixedPoint<12>(app.m_loader.playerHeight); // Load the first scene via PCdrv.
// Files are relative to the pcdrvbase directory (PSXBuild/).
int fileSize = 0;
m_sceneData = psxsplash::SceneLoader::LoadFile("scene_0.splashpack", fileSize);
app.m_input.setOnEvent( if (!m_sceneData) {
eastl::function<void(psyqo::AdvancedPad::Event)>{[this](const psyqo::AdvancedPad::Event& event) { // Fallback: try legacy name for backwards compatibility
if (event.pad != psyqo::AdvancedPad::Pad::Pad1a) return; m_sceneData = psxsplash::SceneLoader::LoadFile("output.bin", fileSize);
if (app.m_loader.navmeshes.empty()) return;
if (event.type == psyqo::AdvancedPad::Event::ButtonPressed) {
if (event.button == psyqo::AdvancedPad::Button::Triangle) {
m_freecam = !m_freecam;
} else if (event.button == psyqo::AdvancedPad::Button::Square) {
m_renderSelect = !m_renderSelect;
} }
}
}});
if (app.m_loader.navmeshes.empty()) { if (m_sceneData) {
m_freecam = true; m_sceneManager.InitializeScene(m_sceneData);
} }
} }
@@ -128,74 +90,12 @@ void MainScene::frame() {
mainScene.m_lastFrameCounter = currentFrameCounter; mainScene.m_lastFrameCounter = currentFrameCounter;
uint8_t rightX = app.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 0); m_sceneManager.GameTick(gpu());
uint8_t rightY = app.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 1);
uint8_t leftX = app.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 2);
uint8_t leftY = app.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 3);
int16_t rightXOffset = (int16_t)rightX - 0x80;
int16_t rightYOffset = (int16_t)rightY - 0x80;
int16_t leftXOffset = (int16_t)leftX - 0x80;
int16_t leftYOffset = (int16_t)leftY - 0x80;
if (__builtin_abs(leftXOffset) < app.m_stickDeadzone &&
__builtin_abs(leftYOffset) < app.m_stickDeadzone) {
m_sprinting = false;
}
if (app.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L3)) {
m_sprinting = true;
}
psyqo::FixedPoint<12> speed = m_sprinting ? sprintSpeed : moveSpeed;
if (__builtin_abs(rightXOffset) > app.m_stickDeadzone) {
camRotY += (rightXOffset * rotSpeed * deltaTime) >> 7;
}
if (__builtin_abs(rightYOffset) > app.m_stickDeadzone) {
camRotX -= (rightYOffset * rotSpeed * deltaTime) >> 7;
camRotX = eastl::clamp(camRotX, -0.5_pi, 0.5_pi);
}
m_mainCamera.SetRotation(camRotX, camRotY, camRotZ);
if (__builtin_abs(leftYOffset) > app.m_stickDeadzone) {
psyqo::FixedPoint<12> forward = -(leftYOffset * speed * deltaTime) >> 7;
m_mainCamera.MoveX((m_trig.sin(camRotY) * forward));
m_mainCamera.MoveZ((m_trig.cos(camRotY) * forward));
}
if (__builtin_abs(leftXOffset) > app.m_stickDeadzone) {
psyqo::FixedPoint<12> strafe = -(leftXOffset * speed * deltaTime) >> 7;
m_mainCamera.MoveX(-(m_trig.cos(camRotY) * strafe));
m_mainCamera.MoveZ((m_trig.sin(camRotY) * strafe));
}
if (app.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1)) {
m_mainCamera.MoveY(speed * deltaTime);
}
if (app.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1)) {
m_mainCamera.MoveY(-speed * deltaTime);
}
if (!m_freecam) {
psyqo::Vec3 adjustedPosition =
psxsplash::ComputeNavmeshPosition(m_mainCamera.GetPosition(), *app.m_loader.navmeshes[0], -pheight);
m_mainCamera.SetPosition(adjustedPosition.x, adjustedPosition.y, adjustedPosition.z);
}
if (!m_renderSelect) {
psxsplash::Renderer::GetInstance().Render(app.m_loader.gameObjects);
} else {
psxsplash::Renderer::GetInstance().RenderNavmeshPreview(*app.m_loader.navmeshes[0], true);
}
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);
gpu().pumpCallbacks(); gpu().pumpCallbacks();
uint32_t endFrame = gpu().now();
uint32_t spent = endFrame - beginFrame;
} }
int main() { return app.run(); } int main() { return app.run(); }

View File

@@ -5,6 +5,9 @@
namespace psxsplash { namespace psxsplash {
// Sentinel value for untextured (vertex-color-only) triangles
static constexpr uint16_t UNTEXTURED_TPAGE = 0xFFFF;
class Tri final { class Tri final {
public: public:
psyqo::GTE::PackedVec3 v0, v1, v2; psyqo::GTE::PackedVec3 v0, v1, v2;
@@ -19,6 +22,12 @@ namespace psxsplash {
uint16_t clutX; uint16_t clutX;
uint16_t clutY; uint16_t clutY;
uint16_t padding; 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;
}
}; };
static_assert(sizeof(Tri) == 52, "Tri is not 52 bytes"); static_assert(sizeof(Tri) == 52, "Tri is not 52 bytes");

View File

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

View File

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

351
src/navregion.cpp Normal file
View File

@@ -0,0 +1,351 @@
#include "navregion.hh"
#include <psyqo/fixed-point.hh>
#include <psyqo/vector.hh>
/**
* navregion.cpp - Convex Region Navigation System
*
* All math is 20.12 fixed-point. Zero floats.
*
* Key operations:
* - resolvePosition: O(1) typical (check current + neighbors via portals)
* - pointInRegion: O(n) per polygon vertices (convex cross test)
* - getFloorY: O(1) plane equation evaluation
* - findRegion: O(R) brute force, used only at init
*/
namespace psxsplash {
// ============================================================================
// Fixed-point helpers
// ============================================================================
static constexpr int FRAC_BITS = 12;
static constexpr int32_t FP_ONE = 1 << FRAC_BITS;
static inline int32_t fpmul(int32_t a, int32_t b) {
return (int32_t)(((int64_t)a * b) >> FRAC_BITS);
}
static inline int32_t fpdiv(int32_t a, int32_t b) {
if (b == 0) return 0;
int32_t q = a / b;
int32_t r = a - q * b;
return q * FP_ONE + (r << FRAC_BITS) / b;
}
// ============================================================================
// Initialization
// ============================================================================
const uint8_t* NavRegionSystem::initializeFromData(const uint8_t* data) {
const auto* hdr = reinterpret_cast<const NavDataHeader*>(data);
m_header = *hdr;
data += sizeof(NavDataHeader);
m_regions = reinterpret_cast<const NavRegion*>(data);
data += m_header.regionCount * sizeof(NavRegion);
m_portals = reinterpret_cast<const NavPortal*>(data);
data += m_header.portalCount * sizeof(NavPortal);
return data;
}
// ============================================================================
// Point-in-convex-polygon (XZ plane)
// ============================================================================
bool NavRegionSystem::pointInConvexPoly(int32_t px, int32_t pz,
const int32_t* vertsX, const int32_t* vertsZ,
int vertCount) {
if (vertCount < 3) return false;
// For CCW winding, all cross products must be >= 0.
// cross = (bx - ax) * (pz - az) - (bz - az) * (px - ax)
for (int i = 0; i < vertCount; i++) {
int next = (i + 1) % vertCount;
int32_t ax = vertsX[i], az = vertsZ[i];
int32_t bx = vertsX[next], bz = vertsZ[next];
// Edge direction
int32_t edgeX = bx - ax;
int32_t edgeZ = bz - az;
// Point relative to edge start
int32_t relX = px - ax;
int32_t relZ = pz - az;
// Cross product (64-bit to prevent overflow)
int64_t cross = (int64_t)edgeX * relZ - (int64_t)edgeZ * relX;
if (cross < 0) return false;
}
return true;
}
// ============================================================================
// Closest point on segment (XZ only)
// ============================================================================
void NavRegionSystem::closestPointOnSegment(int32_t px, int32_t pz,
int32_t ax, int32_t az,
int32_t bx, int32_t bz,
int32_t& outX, int32_t& outZ) {
int32_t abx = bx - ax;
int32_t abz = bz - az;
int32_t lenSq = fpmul(abx, abx) + fpmul(abz, abz);
if (lenSq == 0) {
outX = ax; outZ = az;
return;
}
int32_t dot = fpmul(px - ax, abx) + fpmul(pz - az, abz);
// t = dot / lenSq, clamped to [0, 1]
int32_t t;
if (dot <= 0) {
t = 0;
} else if (dot >= lenSq) {
t = FP_ONE;
} else {
t = fpdiv(dot, lenSq);
}
outX = ax + fpmul(t, abx);
outZ = az + fpmul(t, abz);
}
// ============================================================================
// 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)
// ============================================================================
int32_t NavRegionSystem::getFloorY(int32_t x, int32_t z, uint16_t regionIndex) const {
if (regionIndex >= m_header.regionCount) return 0;
const auto& reg = m_regions[regionIndex];
// Y = planeA * X + planeB * Z + planeD
// (all in 20.12, products need 64-bit intermediate)
return fpmul(reg.planeA, x) + fpmul(reg.planeB, z) + reg.planeD;
}
// ============================================================================
// Point in region test
// ============================================================================
bool NavRegionSystem::pointInRegion(int32_t x, int32_t z, uint16_t regionIndex) const {
if (regionIndex >= m_header.regionCount) return false;
const auto& reg = m_regions[regionIndex];
return pointInConvexPoly(x, z, reg.vertsX, reg.vertsZ, reg.vertCount);
}
// ============================================================================
// Find region (brute force, for initialization)
// ============================================================================
uint16_t NavRegionSystem::findRegion(int32_t x, int32_t z) const {
// When multiple regions overlap at the same XZ position (e.g., floor and
// elevated step), prefer the highest physical surface. In PSX Y-down space,
// highest surface = smallest (most negative) floor Y value.
uint16_t best = NAV_NO_REGION;
int32_t bestY = 0x7FFFFFFF;
for (uint16_t i = 0; i < m_header.regionCount; i++) {
if (pointInRegion(x, z, i)) {
int32_t fy = getFloorY(x, z, i);
if (fy < bestY) {
bestY = fy;
best = i;
}
}
}
return best;
}
// ============================================================================
// Clamp position to region boundary
// ============================================================================
void NavRegionSystem::clampToRegion(int32_t& x, int32_t& z, uint16_t regionIndex) const {
if (regionIndex >= m_header.regionCount) return;
const auto& reg = m_regions[regionIndex];
if (pointInConvexPoly(x, z, reg.vertsX, reg.vertsZ, reg.vertCount))
return; // Already inside
// Find closest point on any edge of the polygon
int32_t bestX = x, bestZ = z;
int64_t bestDistSq = 0x7FFFFFFFFFFFFFFFLL;
for (int i = 0; i < reg.vertCount; i++) {
int next = (i + 1) % reg.vertCount;
int32_t cx, cz;
closestPointOnSegment(x, z,
reg.vertsX[i], reg.vertsZ[i],
reg.vertsX[next], reg.vertsZ[next],
cx, cz);
int64_t dx = (int64_t)(x - cx);
int64_t dz = (int64_t)(z - cz);
int64_t distSq = dx * dx + dz * dz;
if (distSq < bestDistSq) {
bestDistSq = distSq;
bestX = cx;
bestZ = cz;
}
}
x = bestX;
z = bestZ;
}
// ============================================================================
// Resolve position (main per-frame call)
// ============================================================================
int32_t NavRegionSystem::resolvePosition(int32_t& newX, int32_t& newZ,
uint16_t& currentRegion) const {
if (!isLoaded() || m_header.regionCount == 0) return 0;
// If no valid region, find one
if (currentRegion == NAV_NO_REGION || currentRegion >= m_header.regionCount) {
currentRegion = findRegion(newX, newZ);
if (currentRegion == NAV_NO_REGION) return 0;
}
// Check if still in current region
if (pointInRegion(newX, newZ, currentRegion)) {
int32_t fy = getFloorY(newX, newZ, currentRegion);
// Check if a portal neighbor has a higher floor at this position.
// This handles overlapping regions (e.g., floor and elevated step).
// When the player walks onto the step, the step region (portal neighbor)
// has a higher floor (smaller Y in PSX Y-down) and should take priority.
const auto& reg = m_regions[currentRegion];
for (int i = 0; i < reg.portalCount; i++) {
uint16_t portalIdx = reg.portalStart + i;
if (portalIdx >= m_header.portalCount) break;
uint16_t neighbor = m_portals[portalIdx].neighborRegion;
if (neighbor >= m_header.regionCount) continue;
if (pointInRegion(newX, newZ, neighbor)) {
int32_t nfy = getFloorY(newX, newZ, neighbor);
if (nfy < fy) { // Higher physical surface (Y-down: smaller = higher)
currentRegion = neighbor;
fy = nfy;
}
}
}
return fy;
}
// Check portal neighbors
const auto& reg = m_regions[currentRegion];
for (int i = 0; i < reg.portalCount; i++) {
uint16_t portalIdx = reg.portalStart + i;
if (portalIdx >= m_header.portalCount) break;
const auto& portal = m_portals[portalIdx];
uint16_t neighbor = portal.neighborRegion;
if (neighbor < m_header.regionCount && pointInRegion(newX, newZ, neighbor)) {
currentRegion = neighbor;
return getFloorY(newX, newZ, neighbor);
}
}
// Not in current region or any neighbor — try broader search
// This handles jumping/falling to non-adjacent regions (e.g., landing on a platform)
{
uint16_t found = findRegion(newX, newZ);
if (found != NAV_NO_REGION) {
currentRegion = found;
return getFloorY(newX, newZ, found);
}
}
// Truly off all regions — clamp to current region boundary
clampToRegion(newX, newZ, currentRegion);
return getFloorY(newX, newZ, currentRegion);
}
// ============================================================================
// Pathfinding stub
// ============================================================================
bool NavRegionSystem::findPath(uint16_t startRegion, uint16_t endRegion,
NavPath& path) const {
// STUB: Returns false until NPC pathfinding is implemented.
// 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

188
src/navregion.hh Normal file
View File

@@ -0,0 +1,188 @@
#pragma once
/**
* navregion.hh - Convex Region Navigation System
*
* Architecture:
* - Walkable surface decomposed into convex polygonal regions (XZ plane).
* - Adjacent regions share portal edges.
* - Player has a single current region index.
* - Movement: point-in-convex-polygon test → portal crossing → neighbor update.
* - Floor Y: project XZ onto region's floor plane.
* - 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 <psyqo/fixed-point.hh>
#include <psyqo/vector.hh>
namespace psxsplash {
// ============================================================================
// Constants
// ============================================================================
static constexpr int NAV_MAX_VERTS_PER_REGION = 8; // Max polygon verts
static constexpr int NAV_MAX_NEIGHBORS = 8; // Max portal edges per region
static constexpr int NAV_MAX_PATH_STEPS = 32; // Max A* path length
static constexpr uint16_t NAV_NO_REGION = 0xFFFF; // Sentinel: no region
// ============================================================================
// Surface type for navigation regions
// ============================================================================
enum NavSurfaceType : uint8_t {
NAV_SURFACE_FLAT = 0,
NAV_SURFACE_RAMP = 1,
NAV_SURFACE_STAIRS = 2,
};
// ============================================================================
// Portal edge — shared edge between two adjacent regions
// ============================================================================
struct NavPortal {
int32_t ax, az; // Portal edge start (20.12 XZ)
int32_t bx, bz; // Portal edge end (20.12 XZ)
uint16_t neighborRegion; // Index of the region on the other side
int16_t heightDelta; // Vertical step in 4.12 (stairs, ledges)
};
static_assert(sizeof(NavPortal) == 20, "NavPortal must be 20 bytes");
// ============================================================================
// Nav Region — convex polygon on the XZ plane with floor info
// ============================================================================
struct NavRegion {
// Convex polygon vertices (XZ, 20.12 fixed-point)
// Stored in CCW winding order
int32_t vertsX[NAV_MAX_VERTS_PER_REGION]; // 32 bytes
int32_t vertsZ[NAV_MAX_VERTS_PER_REGION]; // 32 bytes
// Floor plane: Y = planeA * X + planeB * Z + planeD (all 20.12)
// For flat floors: planeA = planeB = 0, planeD = floorY
int32_t planeA, planeB, planeD; // 12 bytes
// Portal neighbors
uint16_t portalStart; // Index into portal array 2 bytes
uint8_t portalCount; // Number of portals 1 byte
uint8_t vertCount; // Number of polygon verts 1 byte
// Metadata
NavSurfaceType surfaceType; // 1 byte
uint8_t roomIndex; // Interior room (0xFF = exterior) 1 byte
uint8_t pad[2]; // Alignment 2 bytes
// Total: 32 + 32 + 12 + 4 + 4 = 84 bytes
};
static_assert(sizeof(NavRegion) == 84, "NavRegion must be 84 bytes");
// ============================================================================
// Nav data header
// ============================================================================
struct NavDataHeader {
uint16_t regionCount;
uint16_t portalCount;
uint16_t startRegion; // Region the player spawns in
uint16_t pad;
};
static_assert(sizeof(NavDataHeader) == 8, "NavDataHeader must be 8 bytes");
// ============================================================================
// Path result for A* (used by NPC pathfinding)
// ============================================================================
struct NavPath {
uint16_t regions[NAV_MAX_PATH_STEPS];
int stepCount;
};
// ============================================================================
// NavRegionSystem — manages navigation at runtime
// ============================================================================
class NavRegionSystem {
public:
NavRegionSystem() = default;
/// Initialize from splashpack data. Returns pointer past the data.
const uint8_t* initializeFromData(const uint8_t* data);
/// Is nav data loaded?
bool isLoaded() const { return m_regions != nullptr; }
/// Get the number of regions
uint16_t getRegionCount() const { return m_header.regionCount; }
/// Get the start region
uint16_t getStartRegion() const { return m_header.startRegion; }
/// Get the room index for a given nav region (0xFF = exterior/unknown)
uint8_t getRoomIndex(uint16_t regionIndex) const {
if (m_regions == nullptr || regionIndex >= m_header.regionCount) return 0xFF;
return m_regions[regionIndex].roomIndex;
}
// ========================================================================
// Player movement - called every frame
// ========================================================================
/// Given a new XZ position and the player's current region,
/// determine the correct region and return the floor Y.
/// Updates currentRegion in-place.
/// newX/newZ are clamped to stay within the region boundary.
/// Returns the Y position for the player's feet.
int32_t resolvePosition(int32_t& newX, int32_t& newZ,
uint16_t& currentRegion) const;
/// Test if a point (XZ) is inside a specific region.
bool pointInRegion(int32_t x, int32_t z, uint16_t regionIndex) const;
/// Compute floor Y at a given XZ within a region using the floor plane.
int32_t getFloorY(int32_t x, int32_t z, uint16_t regionIndex) const;
/// Find which region contains a point (brute-force, for initialization).
uint16_t findRegion(int32_t x, int32_t z) const;
/// Clamp an XZ position to stay within a region's polygon boundary.
/// Returns the clamped position.
void clampToRegion(int32_t& x, int32_t& z, uint16_t regionIndex) const;
// ========================================================================
// 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,
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;
const NavPortal* m_portals = nullptr;
/// Point-in-convex-polygon test (XZ plane).
/// Uses cross-product sign consistency (all edges same winding).
static bool pointInConvexPoly(int32_t px, int32_t pz,
const int32_t* vertsX, const int32_t* vertsZ,
int vertCount);
/// Closest point on a line segment to a point (XZ only)
static void closestPointOnSegment(int32_t px, int32_t pz,
int32_t ax, int32_t az,
int32_t bx, int32_t bz,
int32_t& outX, int32_t& outZ);
/// 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

298
src/pcdrv_handler.hh Normal file
View File

@@ -0,0 +1,298 @@
/*
* 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"
namespace psxsplash {
// =========================================================================
// SIO1 hardware registers (UART serial port at 0x1F801050)
// =========================================================================
#define SIO1_DATA (*(volatile uint8_t*)0x1F801050)
#define SIO1_STAT (*(volatile uint32_t*)0x1F801054)
#define SIO1_MODE (*(volatile uint16_t*)0x1F801058)
#define SIO1_CTRL (*(volatile uint16_t*)0x1F80105A)
#define SIO1_BAUD (*(volatile uint16_t*)0x1F80105E)
#define SIO1_TX_RDY (1 << 0)
#define SIO1_RX_RDY (1 << 1)
// =========================================================================
// Low-level SIO1 I/O - blocking, tight polling loops
// =========================================================================
static inline void sio_putc(uint8_t byte) {
while (!(SIO1_STAT & SIO1_TX_RDY)) {}
SIO1_DATA = byte;
}
static inline uint8_t sio_getc() {
while (!(SIO1_STAT & SIO1_RX_RDY)) {}
return SIO1_DATA;
}
static inline void sio_write32(uint32_t val) {
sio_putc((uint8_t)(val));
sio_putc((uint8_t)(val >> 8));
sio_putc((uint8_t)(val >> 16));
sio_putc((uint8_t)(val >> 24));
}
static inline uint32_t sio_read32() {
uint32_t v = (uint32_t)sio_getc();
v |= (uint32_t)sio_getc() << 8;
v |= (uint32_t)sio_getc() << 16;
v |= (uint32_t)sio_getc() << 24;
return v;
}
static inline bool sio_check_okay() {
return sio_getc() == 'O' && sio_getc() == 'K'
&& sio_getc() == 'A' && sio_getc() == 'Y';
}
static inline void sio_pcdrv_escape(uint32_t funcCode) {
sio_putc(0x00);
sio_putc('p');
sio_write32(funcCode);
}
// =========================================================================
// 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);
if (sio_check_okay()) {
sio_getc(); // trailing 0x00
return 0;
}
return -1;
}
static int sio_pcdrv_open(const char* name, int flags) {
sio_pcdrv_escape(0x103);
if (!sio_check_okay()) return -1;
const char* p = name;
while (*p) sio_putc((uint8_t)*p++);
sio_putc(0x00);
sio_write32((uint32_t)flags);
if (sio_check_okay()) {
return (int)sio_read32(); // handle
}
return -1;
}
static int sio_pcdrv_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;
sio_write32((uint32_t)fd);
sio_write32(0); // unused
sio_write32(0); // unused
if (sio_check_okay()) {
sio_read32(); // handle echo
return 0;
}
return -1;
}
static int sio_pcdrv_read(int fd, void* buf, int len) {
sio_pcdrv_escape(0x105);
if (!sio_check_okay()) return -1;
sio_write32((uint32_t)fd);
sio_write32((uint32_t)len);
sio_write32((uint32_t)(uintptr_t)buf); // memaddr for host debug
if (sio_check_okay()) {
uint32_t dataLen = sio_read32();
sio_read32(); // checksum (not verified)
uint8_t* dst = (uint8_t*)buf;
for (uint32_t i = 0; i < dataLen; i++) {
dst[i] = sio_getc();
}
return (int)dataLen;
}
return -1;
}
static int sio_pcdrv_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;
sio_write32((uint32_t)fd);
sio_write32((uint32_t)offset);
sio_write32((uint32_t)whence);
if (sio_check_okay()) {
return (int)sio_read32(); // new position
}
return -1;
}
// =========================================================================
// Public PCDRV API - runtime dispatch between emulator and real hardware
// Use these instead of pcdrv.h functions (PCopen, PCread, etc.)
// =========================================================================
static int pcdrv_init() {
if (pcsx_present()) return PCinit();
return sio_pcdrv_init();
}
static int pcdrv_open(const char* name, int flags, int perms) {
if (pcsx_present()) return PCopen(name, flags, perms);
return sio_pcdrv_open(name, flags);
}
static int pcdrv_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);
}
static int pcdrv_read(int fd, void* buf, int len) {
if (pcsx_present()) return PCread(fd, buf, len);
return sio_pcdrv_read(fd, buf, len);
}
static int pcdrv_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);
}
// =========================================================================
// SIO1 initialization - 115200 baud, 8N1
// =========================================================================
static void sio1Init() {
SIO1_CTRL = 0; // reset
SIO1_MODE = 0x004e; // MUL16, 8 data, no parity, 1 stop
SIO1_BAUD = (uint16_t)(2073600 / 115200); // = 18
SIO1_CTRL = 0x0025; // TX enable, RX enable, RTS assert
for (int i = 0; i < 100; i++) { __asm__ volatile("" ::: "memory"); } // settle delay
}
// =========================================================================
// 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
sio1Init();
// TODO: printf redirect (redirectPrintfToSIO1) disabled for now.
// Printf redirect patches A0 handler machine code at 0xa8/0xb4
// and may cause instability - needs further testing.
}
} // namespace psxsplash

31
src/profiler.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include "profiler.hh"
#ifdef PSXSPLASH_PROFILER
using namespace psxsplash::debug;
// Writes address+name to the PCSX-Redux debugger variable registry.
static void pcsxRegisterVariable(void* address, const char* name) {
register void* a0 asm("a0") = address;
register const char* a1 asm("a1") = name;
__asm__ volatile("sb %0, 0x2081(%1)" : : "r"(255), "r"(0x1f800000), "r"(a0), "r"(a1));
}
void Profiler::initialize() {
reset();
pcsxRegisterVariable(&sectionTimes[0], "profiler.rendering");
pcsxRegisterVariable(&sectionTimes[1], "profiler.lua");
pcsxRegisterVariable(&sectionTimes[2], "profiler.controls");
pcsxRegisterVariable(&sectionTimes[3], "profiler.navmesh");
}
void Profiler::reset() {
for (auto &time : sectionTimes) {
time = 0;
}
}
#endif // PSXSPLASH_PROFILER

47
src/profiler.hh Normal file
View File

@@ -0,0 +1,47 @@
#pragma once
#include <stdint.h>
#ifdef PSXSPLASH_PROFILER
namespace psxsplash::debug {
enum ProfilerSection {
PROFILER_RENDERING,
PROFILER_LUA,
PROFILER_CONTROLS,
PROFILER_NAVMESH,
};
class Profiler {
public:
// Singleton accessor
static Profiler& getInstance() {
static Profiler instance;
return instance;
}
void initialize();
void reset();
void setSectionTime(ProfilerSection section, uint32_t time) {
sectionTimes[section] = time;
}
private:
Profiler() = default;
~Profiler() = default;
// Delete copy/move semantics
Profiler(const Profiler&) = delete;
Profiler& operator=(const Profiler&) = delete;
Profiler(Profiler&&) = delete;
Profiler& operator=(Profiler&&) = delete;
uint32_t sectionTimes[4] = {0, 0, 0, 0};
};
} // namespace psxsplash::debug
#endif // PSXSPLASH_PROFILER

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@
#include <EASTL/array.h> #include <EASTL/array.h>
#include <EASTL/vector.h> #include <EASTL/vector.h>
#include <cstdint>
#include <psyqo/bump-allocator.hh> #include <psyqo/bump-allocator.hh>
#include <psyqo/fragments.hh> #include <psyqo/fragments.hh>
#include <psyqo/gpu.hh> #include <psyqo/gpu.hh>
@@ -14,33 +13,51 @@
#include <psyqo/primitives/triangles.hh> #include <psyqo/primitives/triangles.hh>
#include <psyqo/trigonometry.hh> #include <psyqo/trigonometry.hh>
#include "bvh.hh"
#include "camera.hh" #include "camera.hh"
#include "gameobject.hh" #include "gameobject.hh"
#include "navmesh.hh" #include "triclip.hh"
namespace psxsplash { namespace psxsplash {
struct FogConfig {
bool enabled = false;
psyqo::Color color = {.r = 0, .g = 0, .b = 0};
uint8_t density = 5;
int32_t fogFarSZ = 0;
};
class Renderer final { class Renderer final {
public: public:
Renderer(const Renderer&) = delete; Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete; Renderer& operator=(const Renderer&) = delete;
// FIXME: I have no idea how to precompute the required sizes of these. It would be best to allocate them based on the scene static constexpr size_t ORDERING_TABLE_SIZE = 2048 * 8;
static constexpr size_t ORDERING_TABLE_SIZE = 2048 * 3;
static constexpr size_t BUMP_ALLOCATOR_SIZE = 8096 * 24; static constexpr size_t BUMP_ALLOCATOR_SIZE = 8096 * 24;
static constexpr size_t MAX_VISIBLE_TRIANGLES = 4096;
static constexpr int32_t PROJ_H = 120;
static constexpr int32_t SCREEN_CX = 160;
static constexpr int32_t SCREEN_CY = 120;
static void Init(psyqo::GPU& gpuInstance); static void Init(psyqo::GPU& gpuInstance);
void SetCamera(Camera& camera); void SetCamera(Camera& camera);
void SetFog(const FogConfig& fog);
void Render(eastl::vector<GameObject*>& objects); void Render(eastl::vector<GameObject*>& objects);
void RenderNavmeshPreview(psxsplash::Navmesh navmesh, bool isOnMesh); void RenderWithBVH(eastl::vector<GameObject*>& objects, const BVHManager& bvh);
void RenderWithRooms(eastl::vector<GameObject*>& objects,
const RoomData* rooms, int roomCount,
const PortalData* portals, int portalCount,
const TriangleRef* roomTriRefs,
int cameraRoom = -1);
void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY, int16_t width, int16_t height); void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY,
int16_t width, int16_t height);
static Renderer& GetInstance() { static Renderer& GetInstance() {
psyqo::Kernel::assert(instance != nullptr, "Access to renderer was tried without prior initialization"); psyqo::Kernel::assert(instance != nullptr,
"Access to renderer was tried without prior initialization");
return *instance; return *instance;
} }
@@ -50,8 +67,7 @@ class Renderer final {
Renderer(psyqo::GPU& gpuInstance) : m_gpu(gpuInstance) {} Renderer(psyqo::GPU& gpuInstance) : m_gpu(gpuInstance) {}
~Renderer() {} ~Renderer() {}
Camera* m_currentCamera; Camera* m_currentCamera = nullptr;
psyqo::GPU& m_gpu; psyqo::GPU& m_gpu;
psyqo::Trig<> m_trig; psyqo::Trig<> m_trig;
@@ -59,10 +75,21 @@ class Renderer final {
psyqo::Fragments::SimpleFragment<psyqo::Prim::FastFill> m_clear[2]; psyqo::Fragments::SimpleFragment<psyqo::Prim::FastFill> m_clear[2];
psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE> m_ballocs[2]; psyqo::BumpAllocator<BUMP_ALLOCATOR_SIZE> m_ballocs[2];
FogConfig m_fog;
psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0}; psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0};
void recursiveSubdivideAndRender(Tri &tri, eastl::array<psyqo::Vertex, 3> &projected, int zIndex, TriangleRef m_visibleRefs[MAX_VISIBLE_TRIANGLES];
int maxIterations); 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);
}; };
} // namespace psxsplash } // namespace psxsplash

65
src/sceneloader.cpp Normal file
View File

@@ -0,0 +1,65 @@
#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

49
src/sceneloader.hh Normal file
View File

@@ -0,0 +1,49 @@
#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

617
src/scenemanager.cpp Normal file
View File

@@ -0,0 +1,617 @@
#include "scenemanager.hh"
#include <utility>
#include "collision.hh"
#include "profiler.hh"
#include "renderer.hh"
#include "splashpack.hh"
#include "luaapi.hh"
#include "lua.h"
using namespace psyqo::trig_literals;
using namespace psyqo::fixed_point_literals;
using namespace psxsplash;
void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData) {
L.Reset();
// Initialize audio system
m_audio.init();
// Register the Lua API
LuaAPI::RegisterAll(L.getState(), this);
#ifdef PSXSPLASH_PROFILER
debug::Profiler::getInstance().initialize();
#endif
SplashpackSceneSetup sceneSetup;
m_loader.LoadSplashpack(splashpackData, sceneSetup);
m_luaFiles = std::move(sceneSetup.luaFiles);
m_gameObjects = std::move(sceneSetup.objects);
m_objectNames = std::move(sceneSetup.objectNames);
m_bvh = sceneSetup.bvh; // Copy BVH for frustum culling
m_worldCollision = sceneSetup.worldCollision; // World collision soup (v7+)
m_navRegions = sceneSetup.navRegions; // Nav region system (v7+)
m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION;
// Scene type and render path
m_sceneType = sceneSetup.sceneType;
// Room/portal data for interior scenes (v11+)
m_rooms = sceneSetup.rooms;
m_roomCount = sceneSetup.roomCount;
m_portals = sceneSetup.portals;
m_portalCount = sceneSetup.portalCount;
m_roomTriRefs = sceneSetup.roomTriRefs;
m_roomTriRefCount = sceneSetup.roomTriRefCount;
// Configure fog from splashpack data (v11+)
if (sceneSetup.fogEnabled) {
psxsplash::FogConfig fogCfg;
fogCfg.enabled = true;
fogCfg.color = {.r = sceneSetup.fogR, .g = sceneSetup.fogG, .b = sceneSetup.fogB};
fogCfg.density = sceneSetup.fogDensity;
Renderer::GetInstance().SetFog(fogCfg);
} else {
psxsplash::FogConfig fogCfg;
fogCfg.enabled = false;
Renderer::GetInstance().SetFog(fogCfg);
}
// Copy component arrays
m_interactables = std::move(sceneSetup.interactables);
// Load audio clips into SPU RAM
m_audioClipNames = std::move(sceneSetup.audioClipNames);
for (size_t i = 0; i < sceneSetup.audioClips.size(); i++) {
auto& clip = sceneSetup.audioClips[i];
m_audio.loadClip((int)i, clip.adpcmData, clip.sizeBytes, clip.sampleRate, clip.loop);
}
m_playerPosition = sceneSetup.playerStartPosition;
playerRotationX = 0.0_pi;
playerRotationY = 0.0_pi;
playerRotationZ = 0.0_pi;
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
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_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;
// Convert fixed-point values from binary format to AABB
AABB bounds;
bounds.min.x.value = collider->minX;
bounds.min.y.value = collider->minY;
bounds.min.z.value = collider->minZ;
bounds.max.x.value = collider->maxX;
bounds.max.y.value = collider->maxY;
bounds.max.z.value = collider->maxZ;
// Convert collision type
CollisionType type = static_cast<CollisionType>(collider->collisionType);
// Register with collision system
m_collisionSystem.registerCollider(
collider->gameObjectIndex,
bounds,
type,
collider->layerMask
);
}
// 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);
}
L.RegisterSceneScripts(sceneSetup.sceneLuaFileIndex);
L.OnSceneCreationStart();
// Register game objects
for (auto object : m_gameObjects) {
L.RegisterGameObject(object);
}
m_controls.Init();
Renderer::GetInstance().SetCamera(m_currentCamera);
L.OnSceneCreationEnd();
}
void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) {
LuaAPI::IncrementFrameCount();
// 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
}
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) {
// Get camera room from nav region system (authoritative) instead of AABB guessing.
// NavRegion::roomIndex is set during export from the room each region belongs to.
int camRoom = -1;
if (m_navRegions.isLoaded() && m_playerNavRegion != NAV_NO_REGION) {
uint8_t ri = m_navRegions.getRoomIndex(m_playerNavRegion);
if (ri != 0xFF) camRoom = (int)ri;
}
renderer.RenderWithRooms(m_gameObjects, m_rooms, m_roomCount,
m_portals, m_portalCount, m_roomTriRefs, camRoom);
} else {
renderer.RenderWithBVH(m_gameObjects, m_bvh);
}
gpu.pumpCallbacks();
uint32_t renderingEnd = gpu.now();
uint32_t renderingTime = renderingEnd - renderingStart;
#ifdef PSXSPLASH_PROFILER
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_RENDERING, renderingTime);
#endif
// Collision detection
uint32_t collisionStart = gpu.now();
int collisionCount = m_collisionSystem.detectCollisions();
// Process solid collisions - call OnCollision on BOTH objects
const CollisionResult* results = m_collisionSystem.getResults();
for (int i = 0; i < collisionCount; i++) {
auto* objA = getGameObject(results[i].objectA);
auto* objB = getGameObject(results[i].objectB);
if (objA && objB) {
L.OnCollision(objA, objB);
L.OnCollision(objB, objA); // Call on both objects
}
}
// Process trigger events (enter/stay/exit)
m_collisionSystem.processTriggerEvents(*this);
gpu.pumpCallbacks();
uint32_t collisionEnd = gpu.now();
uint32_t luaStart = gpu.now();
// Lua update tick - call onUpdate for all registered objects with onUpdate handler
for (auto* go : m_gameObjects) {
if (go && go->isActive()) {
L.OnUpdate(go, m_deltaFrames);
}
}
gpu.pumpCallbacks();
uint32_t luaEnd = gpu.now();
uint32_t luaTime = luaEnd - luaStart;
#ifdef PSXSPLASH_PROFILER
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_LUA, luaTime);
#endif
// Update game systems
processEnableDisableEvents();
uint32_t controlsStart = gpu.now();
// Update button state tracking first
m_controls.UpdateButtonStates();
// Update interaction system (checks for interact button press)
updateInteractionSystem();
// Dispatch button events to all objects
uint16_t pressed = m_controls.getButtonsPressed();
uint16_t released = m_controls.getButtonsReleased();
if (pressed || released) {
// Only iterate objects if there are button events
for (auto* go : m_gameObjects) {
if (!go || !go->isActive()) continue;
if (pressed) {
// Dispatch press events for each pressed button
for (int btn = 0; btn < 16; btn++) {
if (pressed & (1 << btn)) {
L.OnButtonPress(go, btn);
}
}
}
if (released) {
// Dispatch release events for each released button
for (int btn = 0; btn < 16; btn++) {
if (released & (1 << btn)) {
L.OnButtonRelease(go, btn);
}
}
}
}
}
// Save position BEFORE movement for collision detection
psyqo::Vec3 oldPlayerPosition = m_playerPosition;
m_controls.HandleControls(m_playerPosition, playerRotationX, playerRotationY, playerRotationZ, freecam, m_deltaFrames);
// Jump input: Cross button triggers jump when grounded
if (m_isGrounded && m_controls.wasButtonPressed(psyqo::AdvancedPad::Button::Cross)) {
m_velocityY = -m_jumpVelocityRaw; // Negative = upward (PSX Y-down)
m_isGrounded = false;
}
gpu.pumpCallbacks();
uint32_t controlsEnd = gpu.now();
uint32_t controlsTime = controlsEnd - controlsStart;
#ifdef PSXSPLASH_PROFILER
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_CONTROLS, controlsTime);
#endif
uint32_t navmeshStart = gpu.now();
if (!freecam) {
// 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);
m_playerPosition.x = slid.x;
m_playerPosition.z = slid.z;
// Apply gravity: velocity changes each frame
for (int f = 0; f < m_deltaFrames; f++) {
m_velocityY += m_gravityPerFrame;
}
// Apply vertical velocity to position
// velocityY is in fp12 per-second; convert per-frame: pos += vel / 30
int32_t posYDelta = (m_velocityY * m_deltaFrames) / 30;
m_playerPosition.y.value += posYDelta;
// Resolve floor Y from nav regions if available
if (m_navRegions.isLoaded()) {
uint16_t prevRegion = m_playerNavRegion;
int32_t px = m_playerPosition.x.value;
int32_t pz = m_playerPosition.z.value;
int32_t floorY = m_navRegions.resolvePosition(
px, pz, m_playerNavRegion);
if (m_playerNavRegion != NAV_NO_REGION) {
m_playerPosition.x.value = px;
m_playerPosition.z.value = pz;
// Ground (feet) position in PSX coords:
// Camera is at position.y, feet are at position.y + playerHeight
// (Y-down: larger Y = lower)
int32_t cameraAtFloor = floorY - m_playerHeight.raw();
if (m_playerPosition.y.value >= cameraAtFloor) {
// Player is at or below floor — snap to ground
m_playerPosition.y.value = cameraAtFloor;
m_velocityY = 0;
m_isGrounded = true;
} else {
// Player is above floor (jumping/airborne)
m_isGrounded = false;
}
} else {
// Off all nav regions — revert to old position
m_playerPosition = oldPlayerPosition;
m_playerNavRegion = prevRegion;
m_velocityY = 0;
m_isGrounded = true;
}
} else {
// Ground trace fallback (no nav regions)
int32_t groundY;
int32_t groundNormalY;
uint8_t surfFlags;
if (m_worldCollision.groundTrace(m_playerPosition,
4096 * 4, // max 4 units down
groundY, groundNormalY, surfFlags, 0xFF)) {
int32_t cameraAtFloor = groundY - m_playerHeight.raw();
if (m_playerPosition.y.value >= cameraAtFloor) {
m_playerPosition.y.value = cameraAtFloor;
m_velocityY = 0;
m_isGrounded = true;
} else {
m_isGrounded = false;
}
} else {
m_playerPosition = oldPlayerPosition;
m_velocityY = 0;
m_isGrounded = true;
}
}
// Ceiling check: if jumping upward, check for ceiling collision
if (m_velocityY < 0 && m_worldCollision.isLoaded()) {
int32_t ceilingY;
if (m_worldCollision.ceilingTrace(m_playerPosition,
m_playerHeight.raw(), ceilingY, 0xFF)) {
// Hit a ceiling — stop upward velocity
m_velocityY = 0;
}
}
}
}
gpu.pumpCallbacks();
uint32_t navmeshEnd = gpu.now();
uint32_t navmeshTime = navmeshEnd - navmeshStart;
#ifdef PSXSPLASH_PROFILER
psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_NAVMESH, navmeshTime);
#endif
m_currentCamera.SetPosition(static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x),
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y),
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z));
m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ);
// Process pending scene transitions (at end of frame)
processPendingSceneLoad();
}
// Trigger event callbacks
void psxsplash::SceneManager::fireTriggerEnter(uint16_t triggerObjIdx, uint16_t otherObjIdx) {
auto* trigger = getGameObject(triggerObjIdx);
auto* other = getGameObject(otherObjIdx);
if (trigger && other) {
L.OnTriggerEnter(trigger, other);
}
}
void psxsplash::SceneManager::fireTriggerStay(uint16_t triggerObjIdx, uint16_t otherObjIdx) {
auto* trigger = getGameObject(triggerObjIdx);
auto* other = getGameObject(otherObjIdx);
if (trigger && other) {
L.OnTriggerStay(trigger, other);
}
}
void psxsplash::SceneManager::fireTriggerExit(uint16_t triggerObjIdx, uint16_t otherObjIdx) {
auto* trigger = getGameObject(triggerObjIdx);
auto* other = getGameObject(otherObjIdx);
if (trigger && other) {
L.OnTriggerExit(trigger, other);
}
}
// ============================================================================
// INTERACTION SYSTEM
// ============================================================================
void psxsplash::SceneManager::updateInteractionSystem() {
// Get interact button state - Cross button by default
auto interactButton = psyqo::AdvancedPad::Button::Cross;
bool buttonPressed = m_controls.wasButtonPressed(interactButton);
if (!buttonPressed) return; // Early out if no interaction attempt
// Player position for distance check
psyqo::FixedPoint<12> playerX = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.x);
psyqo::FixedPoint<12> playerY = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y);
psyqo::FixedPoint<12> playerZ = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z);
// Find closest interactable in range
Interactable* closest = nullptr;
psyqo::FixedPoint<12> closestDistSq;
closestDistSq.value = 0x7FFFFFFF; // Max positive value
for (auto* interactable : m_interactables) {
if (!interactable || !interactable->canInteract()) continue;
// Check if object is active
auto* go = getGameObject(interactable->gameObjectIndex);
if (!go || !go->isActive()) continue;
// Calculate distance squared
psyqo::FixedPoint<12> dx = playerX - interactable->offsetX - go->position.x;
psyqo::FixedPoint<12> dy = playerY - interactable->offsetY - go->position.y;
psyqo::FixedPoint<12> dz = playerZ - interactable->offsetZ - go->position.z;
psyqo::FixedPoint<12> distSq = dx * dx + dy * dy + dz * dz;
// Check if in range and closer than current closest
if (distSq <= interactable->radiusSquared && distSq < closestDistSq) {
closest = interactable;
closestDistSq = distSq;
}
}
// Interact with closest
if (closest != nullptr) {
triggerInteraction(getGameObject(closest->gameObjectIndex));
closest->triggerCooldown();
}
}
void psxsplash::SceneManager::triggerInteraction(GameObject* interactable) {
if (!interactable) return;
L.OnInteract(interactable);
}
// ============================================================================
// ENABLE/DISABLE SYSTEM
// ============================================================================
void psxsplash::SceneManager::setObjectActive(GameObject* go, bool active) {
if (!go) return;
bool wasActive = go->isActive();
if (wasActive == active) return; // No change
go->setActive(active);
// Fire appropriate event
if (active) {
L.OnEnable(go);
} else {
L.OnDisable(go);
}
}
void psxsplash::SceneManager::processEnableDisableEvents() {
// Process any pending enable/disable flags (for batched operations)
for (auto* go : m_gameObjects) {
if (!go) continue;
if (go->isPendingEnable()) {
go->setPendingEnable(false);
if (!go->isActive()) {
go->setActive(true);
L.OnEnable(go);
}
}
if (go->isPendingDisable()) {
go->setPendingDisable(false);
if (go->isActive()) {
go->setActive(false);
L.OnDisable(go);
}
}
}
}
// ============================================================================
// SCENE LOADING (PCdrv multi-scene)
// ============================================================================
void psxsplash::SceneManager::requestSceneLoad(int sceneIndex) {
if (sceneIndex == m_currentSceneIndex) return;
m_pendingSceneIndex = sceneIndex;
}
void psxsplash::SceneManager::processPendingSceneLoad() {
if (m_pendingSceneIndex < 0) return;
int targetIndex = m_pendingSceneIndex;
m_pendingSceneIndex = -1;
// Build filename: scene_N.splashpack
char filename[32];
snprintf(filename, sizeof(filename), "scene_%d.splashpack", targetIndex);
// 1. Tear down EVERYTHING in the current scene first —
// Lua VM, vector backing storage, audio. This returns as much
// heap memory as possible before any new allocation.
clearScene();
// 2. Free old splashpack data BEFORE loading the new one.
// This avoids having both scene buffers in the heap simultaneously,
// which is the primary source of fragmentation that prevents
// the Lua compiler from finding large contiguous blocks.
if (m_currentSceneData) {
SceneLoader::FreeFile(m_currentSceneData);
m_currentSceneData = nullptr;
}
// 3. Allocate new scene data (heap is now maximally consolidated)
int fileSize = 0;
uint8_t* newData = SceneLoader::LoadFile(filename, fileSize);
if (!newData) return;
m_currentSceneData = newData;
m_currentSceneIndex = targetIndex;
// 4. Initialize with new data (creates fresh Lua VM inside)
InitializeScene(newData);
}
void psxsplash::SceneManager::clearScene() {
// 1. Shut down the Lua VM first — frees ALL Lua-allocated memory
// (bytecode, strings, tables, registry) in one shot via lua_close.
L.Shutdown();
// 2. 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.
{ eastl::vector<GameObject*> tmp; tmp.swap(m_gameObjects); }
{ eastl::vector<LuaFile*> tmp; tmp.swap(m_luaFiles); }
{ eastl::vector<const char*> tmp; tmp.swap(m_objectNames); }
{ eastl::vector<const char*> tmp; tmp.swap(m_audioClipNames); }
{ eastl::vector<Interactable*> tmp; tmp.swap(m_interactables); }
// 3. Reset hardware / subsystems
m_audio.reset(); // Free SPU RAM and stop all voices
m_collisionSystem.init(); // Re-init collision system
// BVH, WorldCollision, and NavRegions will be overwritten by next load
// Reset room/portal pointers (they point into splashpack data which is being freed)
m_rooms = nullptr;
m_roomCount = 0;
m_portals = nullptr;
m_portalCount = 0;
m_roomTriRefs = nullptr;
m_roomTriRefCount = 0;
m_sceneType = 0;
}
// ============================================================================
// 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)) {
return m_gameObjects[i];
}
}
return nullptr;
}
int psxsplash::SceneManager::findAudioClipByName(const char* name) const {
if (!name || m_audioClipNames.empty()) return -1;
for (size_t i = 0; i < m_audioClipNames.size(); i++) {
if (m_audioClipNames[i] && name_eq(m_audioClipNames[i], name)) {
return static_cast<int>(i);
}
}
return -1;
}

149
src/scenemanager.hh Normal file
View File

@@ -0,0 +1,149 @@
#pragma once
#include <EASTL/vector.h>
#include <psyqo/trigonometry.hh>
#include <psyqo/vector.hh>
#include <psyqo/gpu.hh>
#include "bvh.hh"
#include "camera.hh"
#include "collision.hh"
#include "controls.hh"
#include "gameobject.hh"
#include "lua.h"
#include "splashpack.hh"
#include "worldcollision.hh"
#include "navregion.hh"
#include "audiomanager.hh"
#include "interactable.hh"
#include "luaapi.hh"
#include "sceneloader.hh"
namespace psxsplash {
class SceneManager {
public:
void InitializeScene(uint8_t* splashpackData);
void GameTick(psyqo::GPU &gpu);
// Trigger event callbacks (called by CollisionSystem)
void fireTriggerEnter(uint16_t triggerObjIdx, uint16_t otherObjIdx);
void fireTriggerStay(uint16_t triggerObjIdx, uint16_t otherObjIdx);
void fireTriggerExit(uint16_t triggerObjIdx, uint16_t otherObjIdx);
// Get game object by index (for collision callbacks)
GameObject* getGameObject(uint16_t index) {
if (index < m_gameObjects.size()) return m_gameObjects[index];
return nullptr;
}
// Get total object count
size_t getGameObjectCount() const { return m_gameObjects.size(); }
// Get object name by index (returns nullptr if no name table or out of range)
const char* getObjectName(uint16_t index) const {
if (index < m_objectNames.size()) return m_objectNames[index];
return nullptr;
}
// Find first object with matching name (linear scan, case-sensitive)
GameObject* findObjectByName(const char* name) const;
// Find audio clip index by name (returns -1 if not found)
int findAudioClipByName(const char* name) const;
// Get audio clip name by index (returns nullptr if out of range)
const char* getAudioClipName(int index) const {
if (index >= 0 && index < (int)m_audioClipNames.size()) return m_audioClipNames[index];
return nullptr;
}
// Public API for game systems
// Interaction system - call from Lua or native code
void triggerInteraction(GameObject* interactable);
// GameObject state control with events
void setObjectActive(GameObject* go, bool active);
// Public accessors for Lua API
Controls& getControls() { return m_controls; }
Camera& getCamera() { return m_currentCamera; }
Lua& getLua() { return L; }
AudioManager& getAudio() { return m_audio; }
// Scene loading (for multi-scene support via PCdrv)
void requestSceneLoad(int sceneIndex);
int getCurrentSceneIndex() const { return m_currentSceneIndex; }
// Check and process pending scene load (called from GameTick)
void processPendingSceneLoad();
private:
psxsplash::Lua L;
psxsplash::SplashPackLoader m_loader;
CollisionSystem m_collisionSystem;
BVHManager m_bvh; // Spatial acceleration for frustum culling
WorldCollision m_worldCollision; // Triangle-level world collision (v7+)
NavRegionSystem m_navRegions; // Convex region navigation (v7+)
uint16_t m_playerNavRegion = NAV_NO_REGION; // Current nav region for player
// Scene type and render path: 0=exterior (BVH), 1=interior (room/portal)
uint16_t m_sceneType = 0;
// Room/portal data (v11+ interior scenes). Pointers into splashpack data.
const RoomData* m_rooms = nullptr;
uint16_t m_roomCount = 0;
const PortalData* m_portals = nullptr;
uint16_t m_portalCount = 0;
const TriangleRef* m_roomTriRefs = nullptr;
uint16_t m_roomTriRefCount = 0;
eastl::vector<LuaFile*> m_luaFiles;
eastl::vector<GameObject*> m_gameObjects;
// Object name table (v9+): parallel to m_gameObjects, points into splashpack data
eastl::vector<const char*> m_objectNames;
// Audio clip name table (v10+): parallel to audio clips, points into splashpack data
eastl::vector<const char*> m_audioClipNames;
// Component arrays
eastl::vector<Interactable*> m_interactables;
// Audio system
AudioManager m_audio;
psxsplash::Controls m_controls;
psxsplash::Camera m_currentCamera;
psyqo::Vec3 m_playerPosition;
psyqo::Angle playerRotationX, playerRotationY, playerRotationZ;
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)
int32_t m_jumpVelocityRaw; // Initial jump velocity in fp12 per second
bool m_isGrounded; // On the ground (can jump)
// Frame timing
uint32_t m_lastFrameTime; // gpu.now() timestamp of previous frame
int m_deltaFrames; // Elapsed frame count (1 normally, 2+ if dropped)
bool freecam = false;
// Scene transition state
int m_currentSceneIndex = 0;
int m_pendingSceneIndex = -1; // -1 = no pending load
uint8_t* m_currentSceneData = nullptr; // Owned pointer to loaded data
// System update methods (called from GameTick)
void updateInteractionSystem();
void processEnableDisableEvents();
void clearScene(); // Deallocate current scene objects
};
}; // namespace psxsplash
// namespace psxsplash

296
src/sio_pcdrv.h Normal file
View File

@@ -0,0 +1,296 @@
/*
* 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

@@ -1,39 +1,77 @@
#include "splashpack.hh" #include "splashpack.hh"
#include <cstdint> #include <EASTL/vector.h>
#include <cstring>
#include <psyqo/fixed-point.hh>
#include <psyqo/gte-registers.hh>
#include <psyqo/primitives/common.hh> #include <psyqo/primitives/common.hh>
#include "bvh.hh"
#include "collision.hh"
#include "gameobject.hh" #include "gameobject.hh"
#include "lua.h" #include "lua.h"
#include "mesh.hh" #include "mesh.hh"
#include "psyqo/fixed-point.hh" #include "worldcollision.hh"
#include "psyqo/gte-registers.hh" #include "navregion.hh"
#include "renderer.hh" #include "renderer.hh"
namespace psxsplash { namespace psxsplash {
struct SPLASHPACKFileHeader { struct SPLASHPACKFileHeader {
char magic[2]; char magic[2]; // "SP"
uint16_t version; uint16_t version; // Format version (8 = movement params)
uint16_t luaFileCount; uint16_t luaFileCount;
uint16_t gameObjectCount; uint16_t gameObjectCount;
uint16_t navmeshCount; uint16_t navmeshCount;
uint16_t textureAtlasCount; uint16_t textureAtlasCount;
uint16_t clutCount; uint16_t clutCount;
uint16_t colliderCount;
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 pad[2]; uint16_t sceneLuaFileIndex;
}; // Version 3 additions:
uint16_t bvhNodeCount;
struct SPLASHPACKLuaFile { uint16_t bvhTriangleRefCount;
union { // Version 4 additions (component counts):
uint32_t luaCodeOffset; uint16_t interactableCount;
const char* luaCode; uint16_t healthCount;
}; uint16_t timerCount;
uint32_t length; uint16_t spawnerCount;
// Version 5 additions (navgrid):
uint16_t hasNavGrid; // 1 if navgrid present, 0 otherwise
uint16_t reserved; // Alignment padding
// Version 6 additions (AABB + scene type):
uint16_t sceneType; // 0 = exterior, 1 = interior
uint16_t reserved2; // Alignment padding
// Version 7 additions (world collision + nav regions):
uint16_t worldCollisionMeshCount;
uint16_t worldCollisionTriCount;
uint16_t navRegionCount;
uint16_t navPortalCount;
// Version 8 additions (movement parameters):
uint16_t moveSpeed; // fp12 per-frame speed constant
uint16_t sprintSpeed; // fp12 per-frame speed constant
uint16_t jumpVelocity; // fp12 per-second initial jump velocity
uint16_t gravity; // fp12 per-second² downward acceleration
uint16_t playerRadius; // fp12 collision radius
uint16_t reserved3; // Alignment padding
// Version 9 additions (object names):
uint32_t nameTableOffset; // Offset to name string table (0 = no names)
// Version 10 additions (audio):
uint16_t audioClipCount; // Number of audio clips
uint16_t reserved4; // Alignment padding
uint32_t audioTableOffset; // Offset to audio clip table (0 = no audio)
// Version 11 additions (fog + room/portal):
uint8_t fogEnabled; // 0 = off, 1 = on
uint8_t fogR, fogG, fogB; // Fog color RGB
uint8_t fogDensity; // 1-10 density scale
uint8_t reserved5; // Alignment
uint16_t roomCount; // 0 = no room system (use BVH path)
uint16_t portalCount;
uint16_t roomTriRefCount;
}; };
static_assert(sizeof(SPLASHPACKFileHeader) == 96, "SPLASHPACKFileHeader must be 96 bytes");
struct SPLASHPACKTextureAtlas { struct SPLASHPACKTextureAtlas {
uint32_t polygonsOffset; uint32_t polygonsOffset;
@@ -49,57 +87,207 @@ struct SPLASHPACKClut {
uint16_t pad; uint16_t pad;
}; };
void SplashPackLoader::LoadSplashpack(uint8_t *data, psxsplash::Lua &lua) { void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup) {
psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null"); psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null");
psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast<psxsplash::SPLASHPACKFileHeader *>(data); psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast<psxsplash::SPLASHPACKFileHeader *>(data);
psyqo::Kernel::assert(memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic"); psyqo::Kernel::assert(__builtin_memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic");
psyqo::Kernel::assert(header->version >= 11, "Splashpack version too old: re-export from SplashEdit");
playerStartPos = header->playerStartPos; setup.playerStartPosition = header->playerStartPos;
playerStartRot = header->playerStartRot; setup.playerStartRotation = header->playerStartRot;
playerHeight = header->playerHeight; setup.playerHeight = header->playerHeight;
gameObjects.reserve(header->gameObjectCount); // Movement parameters (v8+)
navmeshes.reserve(header->navmeshCount); setup.moveSpeed.value = header->moveSpeed;
setup.sprintSpeed.value = header->sprintSpeed;
setup.jumpVelocity.value = header->jumpVelocity;
setup.gravity.value = header->gravity;
setup.playerRadius.value = header->playerRadius;
uint8_t *curentPointer = data + sizeof(psxsplash::SPLASHPACKFileHeader); setup.luaFiles.reserve(header->luaFileCount);
setup.objects.reserve(header->gameObjectCount);
setup.colliders.reserve(header->colliderCount);
// Reserve component arrays (version 4+)
if (header->version >= 4) {
setup.interactables.reserve(header->interactableCount);
}
// V11 header is always 96 bytes (validated by static_assert above).
uint8_t *cursor = data + sizeof(SPLASHPACKFileHeader);
for (uint16_t i = 0; i < header->luaFileCount; i++) { for (uint16_t i = 0; i < header->luaFileCount; i++) {
psxsplash::SPLASHPACKLuaFile *luaHeader = reinterpret_cast<psxsplash::SPLASHPACKLuaFile *>(curentPointer); psxsplash::LuaFile *luaHeader = reinterpret_cast<psxsplash::LuaFile *>(cursor);
luaHeader->luaCode = reinterpret_cast<const char*>(data + luaHeader->luaCodeOffset); luaHeader->luaCode = reinterpret_cast<const char *>(data + luaHeader->luaCodeOffset);
lua.LoadLuaFile(luaHeader->luaCode, luaHeader->length); setup.luaFiles.push_back(luaHeader);
curentPointer += sizeof(psxsplash::SPLASHPACKLuaFile); 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++) { for (uint16_t i = 0; i < header->gameObjectCount; i++) {
psxsplash::GameObject *go = reinterpret_cast<psxsplash::GameObject *>(curentPointer); psxsplash::GameObject *go = reinterpret_cast<psxsplash::GameObject *>(cursor);
go->polygons = reinterpret_cast<psxsplash::Tri *>(data + go->polygonsOffset); go->polygons = reinterpret_cast<psxsplash::Tri *>(data + go->polygonsOffset);
lua.RegisterGameObject(go); setup.objects.push_back(go);
gameObjects.push_back(go); cursor += sizeof(psxsplash::GameObject);
curentPointer += sizeof(psxsplash::GameObject);
} }
for (uint16_t i = 0; i < header->navmeshCount; i++) { // Read collision data (after GameObjects)
psxsplash::Navmesh *navmesh = reinterpret_cast<psxsplash::Navmesh *>(curentPointer); for (uint16_t i = 0; i < header->colliderCount; i++) {
navmesh->polygons = reinterpret_cast<psxsplash::NavMeshTri *>(data + navmesh->polygonsOffset); psxsplash::SPLASHPACKCollider *collider = reinterpret_cast<psxsplash::SPLASHPACKCollider *>(cursor);
navmeshes.push_back(navmesh); setup.colliders.push_back(collider);
curentPointer += sizeof(psxsplash::Navmesh); cursor += sizeof(psxsplash::SPLASHPACKCollider);
} }
// Read BVH data (version 3+)
if (header->version >= 3 && header->bvhNodeCount > 0) {
BVHNode* bvhNodes = reinterpret_cast<BVHNode*>(cursor);
cursor += header->bvhNodeCount * sizeof(BVHNode);
TriangleRef* triangleRefs = reinterpret_cast<TriangleRef*>(cursor);
cursor += header->bvhTriangleRefCount * sizeof(TriangleRef);
setup.bvh.initialize(bvhNodes, header->bvhNodeCount,
triangleRefs, header->bvhTriangleRefCount);
}
// Read component data (version 4+)
if (header->version >= 4) {
// Interactables
for (uint16_t i = 0; i < header->interactableCount; i++) {
psxsplash::Interactable *interactable = reinterpret_cast<psxsplash::Interactable *>(cursor);
setup.interactables.push_back(interactable);
cursor += sizeof(psxsplash::Interactable);
}
// Skip health components (legacy, 24 bytes each)
cursor += header->healthCount * 24;
// Skip timers (legacy, 16 bytes each)
cursor += header->timerCount * 16;
// Skip spawners (legacy, 44 bytes each)
cursor += header->spawnerCount * 44;
}
// Read NavGrid (version 5+ — LEGACY, skip if present)
if (header->version >= 5 && header->hasNavGrid) {
// Skip NavGrid data: header (16 bytes) + cells
// NavGridHeader: 4 int32 = 16 bytes, then gridW*gridH*9 bytes
int32_t* navGridHeader = reinterpret_cast<int32_t*>(cursor);
int32_t gridW = navGridHeader[2];
int32_t gridH = navGridHeader[3];
cursor += 16; // header
cursor += gridW * gridH * 9; // cells (9 bytes each)
// Align to 4 bytes
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
}
// Read world collision soup (version 7+)
if (header->version >= 7 && header->worldCollisionMeshCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.worldCollision.initializeFromData(cursor));
}
// Read nav regions (version 7+)
if (header->version >= 7 && header->navRegionCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.navRegions.initializeFromData(cursor));
}
// Read room/portal data (version 11+, interior scenes)
// Must be read here (after nav regions, before navmesh skip / atlas metadata)
// to match the sequential cursor position where the writer places it.
if (header->version >= 11 && header->roomCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
setup.rooms = reinterpret_cast<const RoomData*>(cursor);
setup.roomCount = header->roomCount;
cursor += header->roomCount * sizeof(RoomData);
setup.portals = reinterpret_cast<const PortalData*>(cursor);
setup.portalCount = header->portalCount;
cursor += header->portalCount * sizeof(PortalData);
setup.roomTriRefs = reinterpret_cast<const TriangleRef*>(cursor);
setup.roomTriRefCount = header->roomTriRefCount;
cursor += header->roomTriRefCount * sizeof(TriangleRef);
}
// Skip legacy navmesh metadata (still present in v7 files)
cursor += header->navmeshCount * 8; // Navmesh struct: 4+2+2 = 8 bytes
for (uint16_t i = 0; i < header->textureAtlasCount; i++) { for (uint16_t i = 0; i < header->textureAtlasCount; i++) {
psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(curentPointer); psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(cursor);
uint8_t *offsetData = data + atlas->polygonsOffset; uint8_t *offsetData = data + atlas->polygonsOffset;
uint16_t *castedData = reinterpret_cast<uint16_t *>(offsetData); uint16_t *castedData = reinterpret_cast<uint16_t *>(offsetData);
psxsplash::Renderer::GetInstance().VramUpload(castedData, atlas->x, atlas->y, atlas->width, atlas->height); psxsplash::Renderer::GetInstance().VramUpload(castedData, atlas->x, atlas->y, atlas->width, atlas->height);
curentPointer += 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 *>(curentPointer); 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, psxsplash::Renderer::GetInstance().VramUpload((uint16_t *)clutOffset, clut->clutPackingX * 16,
clut->clutPackingY, clut->length, 1); clut->clutPackingY, clut->length, 1);
curentPointer += sizeof(psxsplash::SPLASHPACKClut); cursor += sizeof(psxsplash::SPLASHPACKClut);
} }
// Read object name table (version 9+)
if (header->version >= 9 && header->nameTableOffset != 0) {
uint8_t* nameData = data + header->nameTableOffset;
setup.objectNames.reserve(header->gameObjectCount);
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
uint8_t nameLen = *nameData++;
const char* nameStr = reinterpret_cast<const char*>(nameData);
// Names are stored as length-prefixed, null-terminated strings
setup.objectNames.push_back(nameStr);
nameData += nameLen + 1; // +1 for null terminator
}
}
// Read audio clip table (version 10+)
if (header->version >= 10 && header->audioClipCount > 0 && header->audioTableOffset != 0) {
// Audio table: per clip: uint32_t dataOffset, uint32_t sizeBytes, uint16_t sampleRate, uint8_t loop, uint8_t nameLen, uint32_t nameOffset
// Total 16 bytes per entry
uint8_t* audioTable = data + header->audioTableOffset;
setup.audioClips.reserve(header->audioClipCount);
setup.audioClipNames.reserve(header->audioClipCount);
for (uint16_t i = 0; i < header->audioClipCount; i++) {
uint32_t dataOff = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
uint32_t size = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
uint16_t rate = *reinterpret_cast<uint16_t*>(audioTable); audioTable += 2;
uint8_t loop = *audioTable++;
uint8_t nameLen = *audioTable++;
uint32_t nameOff = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
SplashpackSceneSetup::AudioClipSetup clip;
clip.adpcmData = data + dataOff;
clip.sizeBytes = size;
clip.sampleRate = rate;
clip.loop = (loop != 0);
clip.name = (nameLen > 0 && nameOff != 0) ? reinterpret_cast<const char*>(data + nameOff) : nullptr;
setup.audioClips.push_back(clip);
setup.audioClipNames.push_back(clip.name);
}
}
// Read fog configuration (version 11+)
if (header->version >= 11) {
setup.fogEnabled = header->fogEnabled != 0;
setup.fogR = header->fogR;
setup.fogG = header->fogG;
setup.fogB = header->fogB;
setup.fogDensity = header->fogDensity;
}
// Read scene type (version 6+ stored it but it was never read until now)
setup.sceneType = header->sceneType;
} }
} // namespace psxsplash } // namespace psxsplash

295
src/splashpack.cpp.bal Normal file
View File

@@ -0,0 +1,295 @@
#include "splashpack.hh"
#include <EASTL/vector.h>
#include <psyqo/fixed-point.hh>
#include <psyqo/gte-registers.hh>
#include <psyqo/primitives/common.hh>
#include "bvh.hh"
#include "collision.hh"
#include "gameobject.hh"
#include "lua.h"
#include "mesh.hh"
#include "worldcollision.hh"
#include "navregion.hh"
#include "renderer.hh"
namespace psxsplash {
struct SPLASHPACKFileHeader {
char magic[2]; // "SP"
uint16_t version; // Format version (8 = movement params)
uint16_t luaFileCount;
uint16_t gameObjectCount;
uint16_t navmeshCount;
uint16_t textureAtlasCount;
uint16_t clutCount;
uint16_t colliderCount;
psyqo::GTE::PackedVec3 playerStartPos;
psyqo::GTE::PackedVec3 playerStartRot;
psyqo::FixedPoint<12, uint16_t> playerHeight;
uint16_t sceneLuaFileIndex;
// Version 3 additions:
uint16_t bvhNodeCount;
uint16_t bvhTriangleRefCount;
// Version 4 additions (component counts):
uint16_t interactableCount;
uint16_t healthCount;
uint16_t timerCount;
uint16_t spawnerCount;
// Version 5 additions (navgrid):
uint16_t hasNavGrid; // 1 if navgrid present, 0 otherwise
uint16_t reserved; // Alignment padding
// Version 6 additions (AABB + scene type):
uint16_t sceneType; // 0 = exterior, 1 = interior
uint16_t reserved2; // Alignment padding
// Version 7 additions (world collision + nav regions):
uint16_t worldCollisionMeshCount;
uint16_t worldCollisionTriCount;
uint16_t navRegionCount;
uint16_t navPortalCount;
// Version 8 additions (movement parameters):
uint16_t moveSpeed; // fp12 per-frame speed constant
uint16_t sprintSpeed; // fp12 per-frame speed constant
uint16_t jumpVelocity; // fp12 per-second initial jump velocity
uint16_t gravity; // fp12 per-second² downward acceleration
uint16_t playerRadius; // fp12 collision radius
uint16_t reserved3; // Alignment padding
// Version 9 additions (object names):
uint32_t nameTableOffset; // Offset to name string table (0 = no names)
// Version 10 additions (audio):
uint16_t audioClipCount; // Number of audio clips
uint16_t reserved4; // Alignment padding
uint32_t audioTableOffset; // Offset to audio clip table (0 = no audio)
// Version 11 additions (fog + room/portal):
uint8_t fogEnabled; // 0 = off, 1 = on
uint8_t fogR, fogG, fogB; // Fog color RGB
uint8_t fogDensity; // 1-10 density scale
uint8_t reserved5; // Alignment
uint16_t roomCount; // 0 = no room system (use BVH path)
uint16_t portalCount;
uint16_t roomTriRefCount;
};
static_assert(sizeof(SPLASHPACKFileHeader) == 96, "SPLASHPACKFileHeader must be 96 bytes");
struct SPLASHPACKTextureAtlas {
uint32_t polygonsOffset;
uint16_t width, height;
uint16_t x, y;
};
struct SPLASHPACKClut {
uint32_t clutOffset;
uint16_t clutPackingX;
uint16_t clutPackingY;
uint16_t length;
uint16_t pad;
};
void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup) {
psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null");
psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast<psxsplash::SPLASHPACKFileHeader *>(data);
psyqo::Kernel::assert(__builtin_memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic");
psyqo::Kernel::assert(header->version >= 8, "Splashpack version mismatch: re-export from SplashEdit");
setup.playerStartPosition = header->playerStartPos;
setup.playerStartRotation = header->playerStartRot;
setup.playerHeight = header->playerHeight;
// Movement parameters (v8+)
setup.moveSpeed.value = header->moveSpeed;
setup.sprintSpeed.value = header->sprintSpeed;
setup.jumpVelocity.value = header->jumpVelocity;
setup.gravity.value = header->gravity;
setup.playerRadius.value = header->playerRadius;
setup.luaFiles.reserve(header->luaFileCount);
setup.objects.reserve(header->gameObjectCount);
setup.colliders.reserve(header->colliderCount);
// Reserve component arrays (version 4+)
if (header->version >= 4) {
setup.interactables.reserve(header->interactableCount);
}
// V10 header = 84 bytes, V11+ = 96 bytes. sizeof() always returns 96,
// so we must compute the correct offset for older versions.
uint32_t headerSize = (header->version >= 11) ? 96 : 84;
uint8_t *cursor = data + headerSize;
for (uint16_t i = 0; i < header->luaFileCount; i++) {
psxsplash::LuaFile *luaHeader = reinterpret_cast<psxsplash::LuaFile *>(cursor);
luaHeader->luaCode = reinterpret_cast<const char *>(data + luaHeader->luaCodeOffset);
setup.luaFiles.push_back(luaHeader);
cursor += sizeof(psxsplash::LuaFile);
}
// sceneLuaFileIndex is stored as uint16_t in header; 0xFFFF means "no scene script" (-1)
setup.sceneLuaFileIndex = (header->sceneLuaFileIndex == 0xFFFF) ? -1 : (int)header->sceneLuaFileIndex;
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
psxsplash::GameObject *go = reinterpret_cast<psxsplash::GameObject *>(cursor);
go->polygons = reinterpret_cast<psxsplash::Tri *>(data + go->polygonsOffset);
setup.objects.push_back(go);
cursor += sizeof(psxsplash::GameObject);
}
// Read collision data (after GameObjects)
for (uint16_t i = 0; i < header->colliderCount; i++) {
psxsplash::SPLASHPACKCollider *collider = reinterpret_cast<psxsplash::SPLASHPACKCollider *>(cursor);
setup.colliders.push_back(collider);
cursor += sizeof(psxsplash::SPLASHPACKCollider);
}
// Read BVH data (version 3+)
if (header->version >= 3 && header->bvhNodeCount > 0) {
BVHNode* bvhNodes = reinterpret_cast<BVHNode*>(cursor);
cursor += header->bvhNodeCount * sizeof(BVHNode);
TriangleRef* triangleRefs = reinterpret_cast<TriangleRef*>(cursor);
cursor += header->bvhTriangleRefCount * sizeof(TriangleRef);
setup.bvh.initialize(bvhNodes, header->bvhNodeCount,
triangleRefs, header->bvhTriangleRefCount);
}
// Read component data (version 4+)
if (header->version >= 4) {
// Interactables
for (uint16_t i = 0; i < header->interactableCount; i++) {
psxsplash::Interactable *interactable = reinterpret_cast<psxsplash::Interactable *>(cursor);
setup.interactables.push_back(interactable);
cursor += sizeof(psxsplash::Interactable);
}
// Skip health components (legacy, 24 bytes each)
cursor += header->healthCount * 24;
// Skip timers (legacy, 16 bytes each)
cursor += header->timerCount * 16;
// Skip spawners (legacy, 44 bytes each)
cursor += header->spawnerCount * 44;
}
// Read NavGrid (version 5+ — LEGACY, skip if present)
if (header->version >= 5 && header->hasNavGrid) {
// Skip NavGrid data: header (16 bytes) + cells
// NavGridHeader: 4 int32 = 16 bytes, then gridW*gridH*9 bytes
int32_t* navGridHeader = reinterpret_cast<int32_t*>(cursor);
int32_t gridW = navGridHeader[2];
int32_t gridH = navGridHeader[3];
cursor += 16; // header
cursor += gridW * gridH * 9; // cells (9 bytes each)
// Align to 4 bytes
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
}
// Read world collision soup (version 7+)
if (header->version >= 7 && header->worldCollisionMeshCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.worldCollision.initializeFromData(cursor));
}
// Read nav regions (version 7+)
if (header->version >= 7 && header->navRegionCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.navRegions.initializeFromData(cursor));
}
// Read room/portal data (version 11+, interior scenes)
// Must be read here (after nav regions, before navmesh skip / atlas metadata)
// to match the sequential cursor position where the writer places it.
if (header->version >= 11 && header->roomCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
setup.rooms = reinterpret_cast<const RoomData*>(cursor);
setup.roomCount = header->roomCount;
cursor += header->roomCount * sizeof(RoomData);
setup.portals = reinterpret_cast<const PortalData*>(cursor);
setup.portalCount = header->portalCount;
cursor += header->portalCount * sizeof(PortalData);
setup.roomTriRefs = reinterpret_cast<const TriangleRef*>(cursor);
setup.roomTriRefCount = header->roomTriRefCount;
cursor += header->roomTriRefCount * sizeof(TriangleRef);
}
// Skip legacy navmesh metadata (still present in v7 files)
cursor += header->navmeshCount * 8; // Navmesh struct: 4+2+2 = 8 bytes
for (uint16_t i = 0; i < header->textureAtlasCount; i++) {
psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast<psxsplash::SPLASHPACKTextureAtlas *>(cursor);
uint8_t *offsetData = data + atlas->polygonsOffset;
uint16_t *castedData = reinterpret_cast<uint16_t *>(offsetData);
psxsplash::Renderer::GetInstance().VramUpload(castedData, atlas->x, atlas->y, atlas->width, atlas->height);
cursor += sizeof(psxsplash::SPLASHPACKTextureAtlas);
}
for (uint16_t i = 0; i < header->clutCount; i++) {
psxsplash::SPLASHPACKClut *clut = reinterpret_cast<psxsplash::SPLASHPACKClut *>(cursor);
uint8_t *clutOffset = data + clut->clutOffset;
psxsplash::Renderer::GetInstance().VramUpload((uint16_t *)clutOffset, clut->clutPackingX * 16,
clut->clutPackingY, clut->length, 1);
cursor += sizeof(psxsplash::SPLASHPACKClut);
}
// Read object name table (version 9+)
if (header->version >= 9 && header->nameTableOffset != 0) {
uint8_t* nameData = data + header->nameTableOffset;
setup.objectNames.reserve(header->gameObjectCount);
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
uint8_t nameLen = *nameData++;
const char* nameStr = reinterpret_cast<const char*>(nameData);
// Names are stored as length-prefixed, null-terminated strings
setup.objectNames.push_back(nameStr);
nameData += nameLen + 1; // +1 for null terminator
}
}
// Read audio clip table (version 10+)
if (header->version >= 10 && header->audioClipCount > 0 && header->audioTableOffset != 0) {
// Audio table: per clip: uint32_t dataOffset, uint32_t sizeBytes, uint16_t sampleRate, uint8_t loop, uint8_t nameLen, uint32_t nameOffset
// Total 16 bytes per entry
uint8_t* audioTable = data + header->audioTableOffset;
setup.audioClips.reserve(header->audioClipCount);
setup.audioClipNames.reserve(header->audioClipCount);
for (uint16_t i = 0; i < header->audioClipCount; i++) {
uint32_t dataOff = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
uint32_t size = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
uint16_t rate = *reinterpret_cast<uint16_t*>(audioTable); audioTable += 2;
uint8_t loop = *audioTable++;
uint8_t nameLen = *audioTable++;
uint32_t nameOff = *reinterpret_cast<uint32_t*>(audioTable); audioTable += 4;
SplashpackSceneSetup::AudioClipSetup clip;
clip.adpcmData = data + dataOff;
clip.sizeBytes = size;
clip.sampleRate = rate;
clip.loop = (loop != 0);
clip.name = (nameLen > 0 && nameOff != 0) ? reinterpret_cast<const char*>(data + nameOff) : nullptr;
setup.audioClips.push_back(clip);
setup.audioClipNames.push_back(clip.name);
}
}
// Read fog configuration (version 11+)
if (header->version >= 11) {
setup.fogEnabled = header->fogEnabled != 0;
setup.fogR = header->fogR;
setup.fogG = header->fogG;
setup.fogB = header->fogB;
setup.fogDensity = header->fogDensity;
}
// Read scene type (version 6+ stored it but it was never read until now)
setup.sceneType = header->sceneType;
}
} // namespace psxsplash

View File

@@ -2,25 +2,94 @@
#include <EASTL/vector.h> #include <EASTL/vector.h>
#include <cstdint> #include <psyqo/fixed-point.hh>
#include "bvh.hh"
#include "collision.hh"
#include "gameobject.hh" #include "gameobject.hh"
#include "lua.h" #include "lua.h"
#include "navmesh.hh" #include "worldcollision.hh"
#include "psyqo/fixed-point.hh" #include "navregion.hh"
#include "audiomanager.hh"
#include "interactable.hh"
namespace psxsplash { namespace psxsplash {
/**
* Collision data as stored in the binary file (fixed layout for serialization)
* This is the binary-compatible version of CollisionData
*/
struct SPLASHPACKCollider {
// AABB bounds in fixed-point (24 bytes)
int32_t minX, minY, minZ;
int32_t maxX, maxY, maxZ;
// Collision metadata (8 bytes)
uint8_t collisionType; // CollisionType enum
uint8_t layerMask; // Which layers this collides with
uint16_t gameObjectIndex; // Which GameObject this belongs to
uint32_t padding; // Alignment padding
};
static_assert(sizeof(SPLASHPACKCollider) == 32, "SPLASHPACKCollider must be 32 bytes");
struct SplashpackSceneSetup {
int sceneLuaFileIndex;
eastl::vector<LuaFile *> luaFiles;
eastl::vector<GameObject *> objects;
eastl::vector<SPLASHPACKCollider *> colliders;
// 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
struct AudioClipSetup {
const uint8_t* adpcmData;
uint32_t sizeBytes;
uint16_t sampleRate;
bool loop;
const char* name; // Points into splashpack data (null-terminated)
};
eastl::vector<AudioClipSetup> audioClips;
// 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+)
psyqo::GTE::PackedVec3 playerStartPosition;
psyqo::GTE::PackedVec3 playerStartRotation;
psyqo::FixedPoint<12, uint16_t> playerHeight;
// Scene type: 0=exterior (BVH culling), 1=interior (room/portal culling)
uint16_t sceneType = 0;
// Fog configuration (v11+)
bool fogEnabled = false;
uint8_t fogR = 0, fogG = 0, fogB = 0;
uint8_t fogDensity = 5;
// Room/portal data (v11+, interior scenes only)
const RoomData* rooms = nullptr;
uint16_t roomCount = 0;
const PortalData* portals = nullptr;
uint16_t portalCount = 0;
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)
};
class SplashPackLoader { class SplashPackLoader {
public: public:
eastl::vector<GameObject *> gameObjects; void LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup);
eastl::vector<Navmesh *> navmeshes;
psyqo::GTE::PackedVec3 playerStartPos, playerStartRot;
psyqo::FixedPoint<12, uint16_t> playerHeight;
void LoadSplashpack(uint8_t *data, Lua &lua);
}; };
}; // namespace psxsplash }; // namespace psxsplash

197
src/triclip.cpp Normal file
View File

@@ -0,0 +1,197 @@
#include "triclip.hh"
namespace psxsplash {
// ============================================================================
// Screen-space Sutherland-Hodgman clipping
// ============================================================================
// Interpolate between two ClipVertex at parameter t (0..256 = 0.0..1.0, fp8).
// Uses fp8 to avoid overflow with int16 coordinates (int16 * 256 fits int32).
static ClipVertex lerpClip(const ClipVertex& a, const ClipVertex& b, int32_t t) {
ClipVertex r;
r.x = (int16_t)(a.x + (((int32_t)(b.x - a.x) * t) >> 8));
r.y = (int16_t)(a.y + (((int32_t)(b.y - a.y) * t) >> 8));
r.z = (int16_t)(a.z + (((int32_t)(b.z - a.z) * t) >> 8));
r.u = (uint8_t)(a.u + (((int)(b.u) - (int)(a.u)) * t >> 8));
r.v = (uint8_t)(a.v + (((int)(b.v) - (int)(a.v)) * t >> 8));
r.r = (uint8_t)(a.r + (((int)(b.r) - (int)(a.r)) * t >> 8));
r.g = (uint8_t)(a.g + (((int)(b.g) - (int)(a.g)) * t >> 8));
r.b = (uint8_t)(a.b + (((int)(b.b) - (int)(a.b)) * t >> 8));
return r;
}
// Clip a polygon (in[] with inCount vertices) against a single edge.
// Edge is defined by axis (0=X, 1=Y), sign (+1 or -1), and threshold.
// Output written to out[], returns output vertex count.
static int clipEdge(const ClipVertex* in, int inCount,
ClipVertex* out, int axis, int sign, int16_t threshold) {
if (inCount == 0) return 0;
int outCount = 0;
for (int i = 0; i < inCount; i++) {
const ClipVertex& cur = in[i];
const ClipVertex& next = in[(i + 1) % inCount];
int16_t curVal = (axis == 0) ? cur.x : cur.y;
int16_t nextVal = (axis == 0) ? next.x : next.y;
bool curInside = (sign > 0) ? (curVal <= threshold) : (curVal >= threshold);
bool nextInside = (sign > 0) ? (nextVal <= threshold) : (nextVal >= threshold);
if (curInside) {
if (outCount < 8) out[outCount++] = cur;
if (!nextInside) {
// Exiting: compute intersection
int32_t den = (int32_t)nextVal - (int32_t)curVal;
if (den != 0) {
int32_t t = ((int32_t)(threshold - curVal) << 8) / den;
if (t < 0) t = 0;
if (t > 256) t = 256;
if (outCount < 8) out[outCount++] = lerpClip(cur, next, t);
}
}
} else if (nextInside) {
// Entering: compute intersection
int32_t den = (int32_t)nextVal - (int32_t)curVal;
if (den != 0) {
int32_t t = ((int32_t)(threshold - curVal) << 8) / den;
if (t < 0) t = 0;
if (t > 256) t = 256;
if (outCount < 8) out[outCount++] = lerpClip(cur, next, t);
}
}
}
return outCount;
}
int clipTriangle(const ClipVertex& v0, const ClipVertex& v1, const ClipVertex& v2,
ClipResult& result) {
// Working buffers for Sutherland-Hodgman (max 8 vertices after 4 clips).
ClipVertex bufA[8], bufB[8];
bufA[0] = v0; bufA[1] = v1; bufA[2] = v2;
int count = 3;
// Clip against 4 edges: left, right, top, bottom.
count = clipEdge(bufA, count, bufB, 0, -1, CLIP_LEFT); // X >= CLIP_LEFT
count = clipEdge(bufB, count, bufA, 0, +1, CLIP_RIGHT); // X <= CLIP_RIGHT
count = clipEdge(bufA, count, bufB, 1, -1, CLIP_TOP); // Y >= CLIP_TOP
count = clipEdge(bufB, count, bufA, 1, +1, CLIP_BOTTOM); // Y <= CLIP_BOTTOM
if (count < 3) return 0;
// Triangulate the convex polygon into a fan from vertex 0.
int triCount = count - 2;
if (triCount > MAX_CLIP_TRIS) triCount = MAX_CLIP_TRIS;
for (int i = 0; i < triCount; i++) {
result.verts[i * 3 + 0] = bufA[0];
result.verts[i * 3 + 1] = bufA[i + 1];
result.verts[i * 3 + 2] = bufA[i + 2];
}
return triCount;
}
// ============================================================================
// Near-plane (3D view-space) clipping
// ============================================================================
ViewVertex lerpViewVertex(const ViewVertex& a, const ViewVertex& b, int32_t t) {
ViewVertex r;
r.x = a.x + (int32_t)(((int64_t)(b.x - a.x) * t) >> 12);
r.y = a.y + (int32_t)(((int64_t)(b.y - a.y) * t) >> 12);
r.z = a.z + (int32_t)(((int64_t)(b.z - a.z) * t) >> 12);
r.u = (uint8_t)(a.u + (((int)(b.u) - (int)(a.u)) * t >> 12));
r.v = (uint8_t)(a.v + (((int)(b.v) - (int)(a.v)) * t >> 12));
r.r = (uint8_t)(a.r + (((int)(b.r) - (int)(a.r)) * t >> 12));
r.g = (uint8_t)(a.g + (((int)(b.g) - (int)(a.g)) * t >> 12));
r.b = (uint8_t)(a.b + (((int)(b.b) - (int)(a.b)) * t >> 12));
r.pad = 0;
return r;
}
static inline int32_t nearPlaneT(int32_t zA, int32_t zB) {
constexpr int32_t nearZ = (int32_t)NEAR_PLANE_Z << 12;
int32_t num = nearZ - zA;
int32_t den = zB - zA;
if (den == 0) return 0;
int32_t absNum = num < 0 ? -num : num;
int shift = 0;
while (absNum > 0x7FFFF) {
absNum >>= 1;
shift++;
}
if (shift > 0) {
num >>= shift;
den >>= shift;
if (den == 0) return num > 0 ? 4096 : 0;
}
return (num << 12) / den;
}
static inline bool isBehind(int32_t z) {
return z < ((int32_t)NEAR_PLANE_Z << 12);
}
int nearPlaneClip(const ViewVertex& v0, const ViewVertex& v1, const ViewVertex& v2,
NearClipResult& result) {
bool b0 = isBehind(v0.z);
bool b1 = isBehind(v1.z);
bool b2 = isBehind(v2.z);
int behindCount = (int)b0 + (int)b1 + (int)b2;
if (behindCount == 3) {
result.triCount = 0;
return 0;
}
if (behindCount == 0) {
result.triCount = 1;
result.verts[0] = v0;
result.verts[1] = v1;
result.verts[2] = v2;
return 1;
}
if (behindCount == 1) {
const ViewVertex* A;
const ViewVertex* B;
const ViewVertex* C;
if (b0) { A = &v0; B = &v1; C = &v2; }
else if (b1) { A = &v1; B = &v2; C = &v0; }
else { A = &v2; B = &v0; C = &v1; }
int32_t tAB = nearPlaneT(A->z, B->z);
int32_t tAC = nearPlaneT(A->z, C->z);
ViewVertex AB = lerpViewVertex(*A, *B, tAB);
ViewVertex AC = lerpViewVertex(*A, *C, tAC);
result.triCount = 2;
result.verts[0] = AB;
result.verts[1] = *B;
result.verts[2] = *C;
result.verts[3] = AB;
result.verts[4] = *C;
result.verts[5] = AC;
return 2;
}
{
const ViewVertex* A;
const ViewVertex* B;
const ViewVertex* C;
if (!b0) { A = &v0; B = &v1; C = &v2; }
else if (!b1) { A = &v1; B = &v2; C = &v0; }
else { A = &v2; B = &v0; C = &v1; }
int32_t tBA = nearPlaneT(B->z, A->z);
int32_t tCA = nearPlaneT(C->z, A->z);
ViewVertex BA = lerpViewVertex(*B, *A, tBA);
ViewVertex CA = lerpViewVertex(*C, *A, tCA);
result.triCount = 1;
result.verts[0] = *A;
result.verts[1] = BA;
result.verts[2] = CA;
return 1;
}
}
} // namespace psxsplash

116
src/triclip.hh Normal file
View File

@@ -0,0 +1,116 @@
#pragma once
#include <stdint.h>
#include <psyqo/primitives/triangles.hh>
#include <psyqo/primitives/common.hh>
namespace psxsplash {
// ============================================================================
// Screen-space clipping types and functions
// ============================================================================
// Screen-space clip vertex with interpolatable attributes.
struct ClipVertex {
int16_t x, y, z;
uint8_t u, v;
uint8_t r, g, b;
};
// Maximum output triangles from clipping a single triangle against 4 edges.
// Sutherland-Hodgman can produce up to 7 vertices -> 5 triangles in a fan.
static constexpr int MAX_CLIP_TRIS = 5;
// Result of screen-space triangle clipping.
struct ClipResult {
ClipVertex verts[MAX_CLIP_TRIS * 3];
};
// GPU rasterizer limits: max vertex-to-vertex delta.
static constexpr int16_t MAX_DELTA_X = 1023;
static constexpr int16_t MAX_DELTA_Y = 511;
// Screen-space clip region. Must be narrower than rasterizer limits (1023x511)
// so that any triangle fully inside has safe vertex deltas.
// Centered on screen (160,120), extended to half the rasterizer max in each direction.
static constexpr int16_t CLIP_LEFT = 160 - 510; // -350
static constexpr int16_t CLIP_RIGHT = 160 + 510; // 670
static constexpr int16_t CLIP_TOP = 120 - 254; // -134
static constexpr int16_t CLIP_BOTTOM = 120 + 254; // 374
// Check if all 3 vertices are on the same side of any screen edge -> invisible.
inline bool isCompletelyOutside(const psyqo::Vertex& v0,
const psyqo::Vertex& v1,
const psyqo::Vertex& v2) {
int16_t x0 = v0.x, x1 = v1.x, x2 = v2.x;
int16_t y0 = v0.y, y1 = v1.y, y2 = v2.y;
if (x0 < CLIP_LEFT && x1 < CLIP_LEFT && x2 < CLIP_LEFT) return true;
if (x0 > CLIP_RIGHT && x1 > CLIP_RIGHT && x2 > CLIP_RIGHT) return true;
if (y0 < CLIP_TOP && y1 < CLIP_TOP && y2 < CLIP_TOP) return true;
if (y0 > CLIP_BOTTOM && y1 > CLIP_BOTTOM && y2 > CLIP_BOTTOM) return true;
return false;
}
// Check if any vertex is outside the clip region or vertex deltas exceed
// rasterizer limits. If true, the triangle needs screen-space clipping.
inline bool needsClipping(const psyqo::Vertex& v0,
const psyqo::Vertex& v1,
const psyqo::Vertex& v2) {
int16_t x0 = v0.x, x1 = v1.x, x2 = v2.x;
int16_t y0 = v0.y, y1 = v1.y, y2 = v2.y;
// Check if any vertex is outside the clip region.
if (x0 < CLIP_LEFT || x0 > CLIP_RIGHT ||
x1 < CLIP_LEFT || x1 > CLIP_RIGHT ||
x2 < CLIP_LEFT || x2 > CLIP_RIGHT ||
y0 < CLIP_TOP || y0 > CLIP_BOTTOM ||
y1 < CLIP_TOP || y1 > CLIP_BOTTOM ||
y2 < CLIP_TOP || y2 > CLIP_BOTTOM) {
return true;
}
// Check vertex-to-vertex deltas against rasterizer limits.
int16_t minX = x0, maxX = x0;
int16_t minY = y0, maxY = y0;
if (x1 < minX) minX = x1; if (x1 > maxX) maxX = x1;
if (x2 < minX) minX = x2; if (x2 > maxX) maxX = x2;
if (y1 < minY) minY = y1; if (y1 > maxY) maxY = y1;
if (y2 < minY) minY = y2; if (y2 > maxY) maxY = y2;
if ((int32_t)(maxX - minX) > MAX_DELTA_X) return true;
if ((int32_t)(maxY - minY) > MAX_DELTA_Y) return true;
return false;
}
// Sutherland-Hodgman screen-space triangle clipping.
// Clips against CLIP_LEFT/RIGHT/TOP/BOTTOM, then triangulates the result.
// Returns number of output triangles (0 to MAX_CLIP_TRIS), vertices in result.
int clipTriangle(const ClipVertex& v0, const ClipVertex& v1, const ClipVertex& v2,
ClipResult& result);
// ============================================================================
// Near-plane (3D view-space) clipping types and functions
// ============================================================================
#define NEAR_PLANE_Z 48
#define MAX_NEARCLIP_TRIS 2
struct ViewVertex {
int32_t x, y, z;
uint8_t u, v;
uint8_t r, g, b;
uint8_t pad;
};
struct NearClipResult {
int triCount;
ViewVertex verts[MAX_NEARCLIP_TRIS * 3];
};
int nearPlaneClip(const ViewVertex& v0, const ViewVertex& v1, const ViewVertex& v2,
NearClipResult& result);
ViewVertex lerpViewVertex(const ViewVertex& a, const ViewVertex& b, int32_t t);
} // namespace psxsplash

275
src/typestring.h Normal file
View File

@@ -0,0 +1,275 @@
/*~
* Copyright (C) 2015, 2016 George Makrydakis <george@irrequietus.eu>
*
* The 'typestring' header is a single header C++ library for creating types
* to use as type parameters in template instantiations, repository available
* at https://github.com/irrequietus/typestring. Conceptually stemming from
* own implementation of the same thing (but in a more complicated manner to
* be revised) in 'clause': https://github.com/irrequietus/clause.
*
* File subject to the terms and conditions of the Mozilla Public License v 2.0.
* If a copy of the MPLv2 license text was not distributed with this file, you
* can obtain it at: http://mozilla.org/MPL/2.0/.
*/
#ifndef IRQUS_TYPESTRING_HH_
#define IRQUS_TYPESTRING_HH_
namespace irqus {
/*~
* @desc A class 'storing' strings into distinct, reusable compile-time types that
* can be used as type parameters in a template parameter list.
* @tprm C... : char non-type parameter pack whose ordered sequence results
* into a specific string.
* @note Could have wrapped up everything in a single class, eventually will,
* once some compilers fix their class scope lookups! I have added some
* utility functions because asides being a fun little project, it is of
* use in certain constructs related to template metaprogramming
* nonetheless.
*/
template<char... C>
struct typestring final {
private:
static constexpr char const vals[sizeof...(C)+1] = { C...,'\0' };
static constexpr unsigned int sval = sizeof...(C);
public:
static constexpr char const * data() noexcept
{ return &vals[0]; }
static constexpr unsigned int size() noexcept
{ return sval; };
static constexpr char const * cbegin() noexcept
{ return &vals[0]; }
static constexpr char const * cend() noexcept
{ return &vals[sval]; }
};
template<char... C>
constexpr char const typestring<C...>::vals[sizeof...(C)+1];
//*~ part 1: preparing the ground, because function templates are awesome.
/*~
* @note While it is easy to resort to constexpr strings for use in constexpr
* metaprogramming, what we want is to convert compile time string in situ
* definitions into reusable, distinct types, for use in advanced template
* metaprogramming techniques. We want such features because this kind of
* metaprogramming constitutes a pure, non-strict, untyped functional
* programming language with pattern matching where declarative semantics
* can really shine.
*
* Currently, there is no feature in C++ that offers the opportunity to
* use strings as type parameter types themselves, despite there are
* several, different library implementations. This implementation is a
* fast, short, single-header, stupid-proof solution that works with any
* C++11 compliant compiler and up, with the resulting type being easily
* reusable throughout the code.
*
* @usge Just include the header and enable -std=c++11 or -std=c++14 etc, use
* like in the following example:
*
* typestring_is("Hello!")
*
* is essentially identical to the following template instantiation:
*
* irqus::typestring<'H', 'e', 'l', 'l', 'o', '!'>
*
* By passing -DUSE_TYPESTRING=<power of 2> during compilation, you can
* set the maximum length of the 'typestring' from 1 to 1024 (2^0 to 2^10).
* Although all preprocessor implementations tested are capable of far
* more with this method, exceeding this limit may cause internal compiler
* errors in most, with at times rather hilarious results.
*/
template<int N, int M>
constexpr char tygrab(char const(&c)[M]) noexcept
{ return c[N < M ? N : M-1]; }
//*~ part2: Function template type signatures for type deduction purposes. In
// other words, exploiting the functorial nature of parameter packs
// while mixing them with an obvious catamorphism through pattern
// matching galore (partial ordering in this case in C++ "parlance").
template<char... X>
auto typoke(typestring<X...>) // as is...
-> typestring<X...>;
template<char... X, char... Y>
auto typoke(typestring<X...>, typestring<'\0'>, typestring<Y>...)
-> typestring<X...>;
template<char A, char... X, char... Y>
auto typoke(typestring<X...>, typestring<A>, typestring<Y>...)
-> decltype(typoke(typestring<X...,A>(), typestring<Y>()...));
template<char... C>
auto typeek(typestring<C...>)
-> decltype(typoke(typestring<C>()...));
template<char... A, char... B, typename... X>
auto tycat_(typestring<A...>, typestring<B...>, X... x)
-> decltype(tycat_(typestring<A..., B...>(), x...));
template<char... X>
auto tycat_(typestring<X...>)
-> typestring<X...>;
/*
* Some people actually using this header as is asked me to include
* a typestring "cat" utility given that it is easy enough to implement.
* I have added this functionality through the template alias below. For
* the obvious implementation, nothing more to say. All T... must be
* of course, "typestrings".
*/
template<typename... T>
using tycat
= decltype(tycat_(T()...));
} /* irqus */
//*~ part3: some necessary code generation using preprocessor metaprogramming!
// There is functional nature in preprocessor metaprogramming as well.
/*~
* @note Code generation block. Undoubtedly, the preprocessor implementations
* of both clang++ and g++ are relatively competent in producing a
* relatively adequate amount of boilerplate for implementing features
* that the language itself will probably be having as features in a few
* years. At times, like herein, the preprocessor is able to generate
* boilerplate *extremely* fast, but over a certain limit the compiler is
* incapable of compiling it. For the record, only certain versions of
* g++ where capable of going beyond 4K, so I thought of going from base
* 16 to base 2 for USE_TYPESTRING power base. For the record, it takes
* a few milliseconds to generate boilerplate for several thousands worth
* of "string" length through such an 'fmap' like procedure.
*/
/* 2^0 = 1 */
#define TYPESTRING1(n,x) irqus::tygrab<0x##n##0>(x)
/* 2^1 = 2 */
#define TYPESTRING2(n,x) irqus::tygrab<0x##n##0>(x), irqus::tygrab<0x##n##1>(x)
/* 2^2 = 2 */
#define TYPESTRING4(n,x) \
irqus::tygrab<0x##n##0>(x), irqus::tygrab<0x##n##1>(x) \
, irqus::tygrab<0x##n##2>(x), irqus::tygrab<0x##n##3>(x)
/* 2^3 = 8 */
#define TYPESTRING8(n,x) \
irqus::tygrab<0x##n##0>(x), irqus::tygrab<0x##n##1>(x) \
, irqus::tygrab<0x##n##2>(x), irqus::tygrab<0x##n##3>(x) \
, irqus::tygrab<0x##n##4>(x), irqus::tygrab<0x##n##5>(x) \
, irqus::tygrab<0x##n##6>(x), irqus::tygrab<0x##n##7>(x)
/* 2^4 = 16 */
#define TYPESTRING16(n,x) \
irqus::tygrab<0x##n##0>(x), irqus::tygrab<0x##n##1>(x) \
, irqus::tygrab<0x##n##2>(x), irqus::tygrab<0x##n##3>(x) \
, irqus::tygrab<0x##n##4>(x), irqus::tygrab<0x##n##5>(x) \
, irqus::tygrab<0x##n##6>(x), irqus::tygrab<0x##n##7>(x) \
, irqus::tygrab<0x##n##8>(x), irqus::tygrab<0x##n##9>(x) \
, irqus::tygrab<0x##n##A>(x), irqus::tygrab<0x##n##B>(x) \
, irqus::tygrab<0x##n##C>(x), irqus::tygrab<0x##n##D>(x) \
, irqus::tygrab<0x##n##E>(x), irqus::tygrab<0x##n##F>(x)
/* 2^5 = 32 */
#define TYPESTRING32(n,x) \
TYPESTRING16(n##0,x),TYPESTRING16(n##1,x)
/* 2^6 = 64 */
#define TYPESTRING64(n,x) \
TYPESTRING16(n##0,x), TYPESTRING16(n##1,x), TYPESTRING16(n##2,x) \
, TYPESTRING16(n##3,x)
/* 2^7 = 128 */
#define TYPESTRING128(n,x) \
TYPESTRING16(n##0,x), TYPESTRING16(n##1,x), TYPESTRING16(n##2,x) \
, TYPESTRING16(n##3,x), TYPESTRING16(n##4,x), TYPESTRING16(n##5,x) \
, TYPESTRING16(n##6,x), TYPESTRING16(n##7,x)
/* 2^8 = 256 */
#define TYPESTRING256(n,x) \
TYPESTRING16(n##0,x), TYPESTRING16(n##1,x), TYPESTRING16(n##2,x) \
, TYPESTRING16(n##3,x), TYPESTRING16(n##4,x), TYPESTRING16(n##5,x) \
, TYPESTRING16(n##6,x), TYPESTRING16(n##7,x), TYPESTRING16(n##8,x) \
, TYPESTRING16(n##9,x), TYPESTRING16(n##A,x), TYPESTRING16(n##B,x) \
, TYPESTRING16(n##C,x), TYPESTRING16(n##D,x), TYPESTRING16(n##E,x) \
, TYPESTRING16(n##F,x)
/* 2^9 = 512 */
#define TYPESTRING512(n,x) \
TYPESTRING256(n##0,x), TYPESTRING256(n##1,x)
/* 2^10 = 1024 */
#define TYPESTRING1024(n,x) \
TYPESTRING256(n##0,x), TYPESTRING256(n##1,x), TYPESTRING256(n##2,x) \
, TYPESTRING128(n##3,x), TYPESTRING16(n##38,x), TYPESTRING16(n##39,x) \
, TYPESTRING16(n##3A,x), TYPESTRING16(n##3B,x), TYPESTRING16(n##3C,x) \
, TYPESTRING16(n##3D,x), TYPESTRING16(n##3E,x), TYPESTRING16(n##3F,x)
//*~ part4 : Let's give some logic with a -DUSE_TYPESTRING flag!
#ifdef USE_TYPESTRING
#if USE_TYPESTRING == 0
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING1(,x)>()))
#elif USE_TYPESTRING == 1
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING2(,x)>()))
#elif USE_TYPESTRING == 2
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING4(,x)>()))
#elif USE_TYPESTRING == 3
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING8(,x)>()))
#elif USE_TYPESTRING == 4
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING16(,x)>()))
#elif USE_TYPESTRING == 5
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING32(,x)>()))
#elif USE_TYPESTRING == 6
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING64(,x)>()))
#elif USE_TYPESTRING == 7
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING128(,x)>()))
#elif USE_TYPESTRING == 8
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING256(,x)>()))
#elif USE_TYPESTRING == 9
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING512(,x)>()))
#elif USE_TYPESTRING == 10
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING1024(,x)>()))
#elif USE_TYPESTRING > 10
#warning !!!: custom typestring length exceeded allowed (1024) !!!
#warning !!!: all typestrings to default maximum typestring length of 64 !!!
#warning !!!: you can use -DUSE_TYPESTRING=<power of two> to set length !!!
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING64(,x)>()))
#elif USE_TYPESTRING < 0
#warning !!!: You used USE_TYPESTRING with a negative size specified !!!
#warning !!!: all typestrings to default maximum typestring length of 64 !!!
#warning !!!: you can use -DUSE_TYPESTRING=<power of two> to set length !!!
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING64(,x)>()))
#endif
#else
#define typestring_is(x) \
decltype(irqus::typeek(irqus::typestring<TYPESTRING64(,x)>()))
#endif
#endif /* IRQUS_TYPESTRING_HH_ */

12
src/vram_config.h Normal file
View File

@@ -0,0 +1,12 @@
// Auto-generated by SplashEdit - do not edit manually.
#pragma once
// GPU resolution
#define VRAM_RES_WIDTH 320
#define VRAM_RES_HEIGHT 240
#define VRAM_RES_ENUM psyqo::GPU::Resolution::W320
#define VRAM_INTERLACE psyqo::GPU::Interlace::PROGRESSIVE
// Framebuffer layout
#define VRAM_DUAL_BUFFER 1
#define VRAM_VERTICAL 1

621
src/worldcollision.cpp Normal file
View File

@@ -0,0 +1,621 @@
#include "worldcollision.hh"
#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 {
// ============================================================================
// Fixed-point helpers (20.12)
// ============================================================================
static constexpr int FRAC_BITS = 12;
static constexpr int32_t FP_ONE = 1 << FRAC_BITS; // 4096
// Multiply two 20.12 values → 20.12
static inline int32_t fpmul(int32_t a, int32_t b) {
return (int32_t)(((int64_t)a * b) >> FRAC_BITS);
}
// Fixed-point division: returns (a << 12) / b using only 32-bit DIV.
// Uses remainder theorem: (a * 4096) / b = (a/b)*4096 + ((a%b)*4096)/b
static inline int32_t fpdiv(int32_t a, int32_t b) {
if (b == 0) return 0;
int32_t q = a / b;
int32_t r = a - q * b;
// r * FP_ONE is safe when |r| < 524288 (which covers most game values)
return q * FP_ONE + (r << FRAC_BITS) / b;
}
// Dot product of two 3-vectors in 20.12
static inline int32_t dot3(int32_t ax, int32_t ay, int32_t az,
int32_t bx, int32_t by, int32_t bz) {
return (int32_t)((((int64_t)ax * bx) + ((int64_t)ay * by) + ((int64_t)az * bz)) >> FRAC_BITS);
}
// Cross product components (each result is 20.12)
static inline void cross3(int32_t ax, int32_t ay, int32_t az,
int32_t bx, int32_t by, int32_t bz,
int32_t& rx, int32_t& ry, int32_t& rz) {
rx = (int32_t)(((int64_t)ay * bz - (int64_t)az * by) >> FRAC_BITS);
ry = (int32_t)(((int64_t)az * bx - (int64_t)ax * bz) >> FRAC_BITS);
rz = (int32_t)(((int64_t)ax * by - (int64_t)ay * bx) >> FRAC_BITS);
}
// Square root approximation via Newton's method (for 20.12 input)
static int32_t fpsqrt(int32_t x) {
if (x <= 0) return 0;
// Initial guess: shift right by 6 (half of 12 fractional bits, then adjust)
int32_t guess = x;
// Rough initial guess
if (x > FP_ONE * 16) guess = x >> 4;
else if (x > FP_ONE) guess = x >> 2;
else guess = FP_ONE;
// Newton iterations: guess = (guess + x/guess) / 2 in fixed-point
for (int i = 0; i < 8; i++) {
if (guess == 0) return 0;
int32_t div = fpdiv(x, guess);
guess = (guess + div) >> 1;
}
return guess;
}
// Length squared of vector (result in 20.12, but represents a squared quantity)
static inline int32_t lengthSq(int32_t x, int32_t y, int32_t z) {
return dot3(x, y, z, x, y, z);
}
// Clamp value to [lo, hi]
static inline int32_t fpclamp(int32_t val, int32_t lo, int32_t hi) {
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}
// ============================================================================
// Initialization
// ============================================================================
const uint8_t* WorldCollision::initializeFromData(const uint8_t* data) {
// Read header
const auto* hdr = reinterpret_cast<const CollisionDataHeader*>(data);
m_header = *hdr;
data += sizeof(CollisionDataHeader);
// Mesh headers
m_meshes = reinterpret_cast<const CollisionMeshHeader*>(data);
data += m_header.meshCount * sizeof(CollisionMeshHeader);
// Triangles
m_triangles = reinterpret_cast<const CollisionTri*>(data);
data += m_header.triangleCount * sizeof(CollisionTri);
// Spatial chunks (exterior only)
if (m_header.chunkGridW > 0 && m_header.chunkGridH > 0) {
m_chunks = reinterpret_cast<const CollisionChunk*>(data);
data += m_header.chunkGridW * m_header.chunkGridH * sizeof(CollisionChunk);
} else {
m_chunks = nullptr;
}
return data;
}
// ============================================================================
// Broad phase: gather candidate meshes
// ============================================================================
int WorldCollision::gatherCandidateMeshes(int32_t posX, int32_t posZ,
uint8_t currentRoom,
uint16_t* outIndices,
int maxIndices) const {
int count = 0;
if (m_chunks && m_header.chunkGridW > 0) {
// Exterior: spatial grid lookup
// dividing two 20.12 values gives integer grid coords directly
int cx = 0, cz = 0;
if (m_header.chunkSize > 0) {
cx = (posX - m_header.chunkOriginX) / m_header.chunkSize;
cz = (posZ - m_header.chunkOriginZ) / m_header.chunkSize;
}
// Check 3x3 neighborhood for robustness
for (int dz = -1; dz <= 1 && count < maxIndices; dz++) {
for (int dx = -1; dx <= 1 && count < maxIndices; dx++) {
int gx = cx + dx;
int gz = cz + dz;
if (gx < 0 || gx >= m_header.chunkGridW || gz < 0 || gz >= m_header.chunkGridH)
continue;
const auto& chunk = m_chunks[gz * m_header.chunkGridW + gx];
for (int i = 0; i < chunk.meshCount && count < maxIndices; i++) {
uint16_t mi = chunk.firstMeshIndex + i;
if (mi < m_header.meshCount) {
// Deduplicate: check if already added
bool dup = false;
for (int k = 0; k < count; k++) {
if (outIndices[k] == mi) { dup = true; break; }
}
if (!dup) {
outIndices[count++] = mi;
}
}
}
}
}
} else {
// Interior: filter by room index, or test all if no room system
for (uint16_t i = 0; i < m_header.meshCount && count < maxIndices; i++) {
if (currentRoom == 0xFF || m_meshes[i].roomIndex == currentRoom ||
m_meshes[i].roomIndex == 0xFF) {
outIndices[count++] = i;
}
}
}
return count;
}
// ============================================================================
// AABB helpers
// ============================================================================
bool WorldCollision::aabbOverlap(int32_t aMinX, int32_t aMinY, int32_t aMinZ,
int32_t aMaxX, int32_t aMaxY, int32_t aMaxZ,
int32_t bMinX, int32_t bMinY, int32_t bMinZ,
int32_t bMaxX, int32_t bMaxY, int32_t bMaxZ) {
return aMinX <= bMaxX && aMaxX >= bMinX &&
aMinY <= bMaxY && aMaxY >= bMinY &&
aMinZ <= bMaxZ && aMaxZ >= bMinZ;
}
void WorldCollision::sphereToAABB(int32_t cx, int32_t cy, int32_t cz, int32_t r,
int32_t& minX, int32_t& minY, int32_t& minZ,
int32_t& maxX, int32_t& maxY, int32_t& maxZ) {
minX = cx - r; minY = cy - r; minZ = cz - r;
maxX = cx + r; maxY = cy + r; maxZ = cz + r;
}
// ============================================================================
// Sphere vs Triangle (closest point approach)
// ============================================================================
int32_t WorldCollision::sphereVsTriangle(int32_t cx, int32_t cy, int32_t cz,
int32_t radius,
const CollisionTri& tri,
int32_t& outNx, int32_t& outNy, int32_t& outNz) const {
// Compute vector from v0 to sphere center
int32_t px = cx - tri.v0x;
int32_t py = cy - tri.v0y;
int32_t pz = cz - tri.v0z;
// Project onto triangle plane using precomputed normal
int32_t dist = dot3(px, py, pz, tri.nx, tri.ny, tri.nz);
// Quick reject if too far from plane
int32_t absDist = dist >= 0 ? dist : -dist;
if (absDist > radius) return 0;
// Find closest point on triangle to sphere center
// Use barycentric coordinates via edge projections
// Precompute edge dot products for barycentric coords
int32_t d00 = dot3(tri.e1x, tri.e1y, tri.e1z, tri.e1x, tri.e1y, tri.e1z);
int32_t d01 = dot3(tri.e1x, tri.e1y, tri.e1z, tri.e2x, tri.e2y, tri.e2z);
int32_t d11 = dot3(tri.e2x, tri.e2y, tri.e2z, tri.e2x, tri.e2y, tri.e2z);
int32_t d20 = dot3(px, py, pz, tri.e1x, tri.e1y, tri.e1z);
int32_t d21 = dot3(px, py, pz, tri.e2x, tri.e2y, tri.e2z);
// Barycentric denom using fpmul (stays in 32-bit)
int32_t denom = fpmul(d00, d11) - fpmul(d01, d01);
if (denom == 0) return 0; // Degenerate triangle
// Barycentric numerators (32-bit via fpmul)
int32_t uNum = fpmul(d11, d20) - fpmul(d01, d21);
int32_t vNum = fpmul(d00, d21) - fpmul(d01, d20);
// u, v in 20.12 using 32-bit division only
int32_t u = fpdiv(uNum, denom);
int32_t v = fpdiv(vNum, denom);
// Clamp to triangle
int32_t w = FP_ONE - u - v;
int32_t closestX, closestY, closestZ;
if (u >= 0 && v >= 0 && w >= 0) {
// Point is inside triangle — closest point is the plane projection
closestX = cx - fpmul(dist, tri.nx);
closestY = cy - fpmul(dist, tri.ny);
closestZ = cz - fpmul(dist, tri.nz);
} else {
// Point is outside triangle — find closest point on edges/vertices
// Check all 3 edges and pick the closest point
// v1 = v0 + e1, v2 = v0 + e2
int32_t v1x = tri.v0x + tri.e1x;
int32_t v1y = tri.v0y + tri.e1y;
int32_t v1z = tri.v0z + tri.e1z;
int32_t v2x = tri.v0x + tri.e2x;
int32_t v2y = tri.v0y + tri.e2y;
int32_t v2z = tri.v0z + tri.e2z;
int32_t bestDistSq = 0x7FFFFFFF;
closestX = tri.v0x;
closestY = tri.v0y;
closestZ = tri.v0z;
// Helper lambda: closest point on segment [A, B] to point P
auto closestOnSeg = [&](int32_t ax, int32_t ay, int32_t az,
int32_t bx, int32_t by, int32_t bz,
int32_t& ox, int32_t& oy, int32_t& oz) {
int32_t abx = bx - ax, aby = by - ay, abz = bz - az;
int32_t apx = cx - ax, apy = cy - ay, apz = cz - az;
int32_t abLen = dot3(abx, aby, abz, abx, aby, abz);
if (abLen == 0) { ox = ax; oy = ay; oz = az; return; }
int32_t dotAP = dot3(apx, apy, apz, abx, aby, abz);
int32_t t = fpclamp(fpdiv(dotAP, abLen), 0, FP_ONE);
ox = ax + fpmul(t, abx);
oy = ay + fpmul(t, aby);
oz = az + fpmul(t, abz);
};
// Edge v0→v1
int32_t ex, ey, ez;
closestOnSeg(tri.v0x, tri.v0y, tri.v0z, v1x, v1y, v1z, ex, ey, ez);
int32_t dx = cx - ex, dy = cy - ey, dz = cz - ez;
int32_t dsq = lengthSq(dx, dy, dz);
if (dsq < bestDistSq) { bestDistSq = dsq; closestX = ex; closestY = ey; closestZ = ez; }
// Edge v0→v2
closestOnSeg(tri.v0x, tri.v0y, tri.v0z, v2x, v2y, v2z, ex, ey, ez);
dx = cx - ex; dy = cy - ey; dz = cz - ez;
dsq = lengthSq(dx, dy, dz);
if (dsq < bestDistSq) { bestDistSq = dsq; closestX = ex; closestY = ey; closestZ = ez; }
// Edge v1→v2
closestOnSeg(v1x, v1y, v1z, v2x, v2y, v2z, ex, ey, ez);
dx = cx - ex; dy = cy - ey; dz = cz - ez;
dsq = lengthSq(dx, dy, dz);
if (dsq < bestDistSq) { bestDistSq = dsq; closestX = ex; closestY = ey; closestZ = ez; }
}
// Compute vector from closest point to sphere center
int32_t nx = cx - closestX;
int32_t ny = cy - closestY;
int32_t nz = cz - closestZ;
// Use 64-bit for distance-squared comparison to avoid 20.12 underflow.
// With small radii (e.g. radius=12 for 0.3m at GTE100), fpmul(12,12)=0
// because 144>>12=0. This caused ALL collisions to silently fail.
// Both sides are in the same raw scale (no shift needed for comparison).
int64_t rawDistSq = (int64_t)nx * nx + (int64_t)ny * ny + (int64_t)nz * nz;
int64_t rawRadSq = (int64_t)radius * radius;
if (rawDistSq >= rawRadSq || rawDistSq == 0) return 0;
// For the actual distance value, use fpsqrt on the 20.12 representation.
// If the 20.12 value underflows to 0, estimate from 64-bit.
int32_t distSq32 = (int32_t)(rawDistSq >> FRAC_BITS);
int32_t distance;
if (distSq32 > 0) {
distance = fpsqrt(distSq32);
} else {
// Very close collision - distance is sub-unit in 20.12.
// Use triangle normal as push direction, penetration = radius.
outNx = tri.nx;
outNy = tri.ny;
outNz = tri.nz;
return radius;
}
if (distance == 0) {
outNx = tri.nx;
outNy = tri.ny;
outNz = tri.nz;
return radius;
}
// Normalize push direction using 32-bit division only
outNx = fpdiv(nx, distance);
outNy = fpdiv(ny, distance);
outNz = fpdiv(nz, distance);
return radius - distance; // Penetration depth
}
// ============================================================================
// Ray vs Triangle (Möller-Trumbore, fixed-point)
// ============================================================================
int32_t WorldCollision::rayVsTriangle(int32_t ox, int32_t oy, int32_t oz,
int32_t dx, int32_t dy, int32_t dz,
const CollisionTri& tri) const {
// h = cross(D, e2)
int32_t hx, hy, hz;
cross3(dx, dy, dz, tri.e2x, tri.e2y, tri.e2z, hx, hy, hz);
// a = dot(e1, h)
int32_t a = dot3(tri.e1x, tri.e1y, tri.e1z, hx, hy, hz);
if (a > -COLLISION_EPSILON && a < COLLISION_EPSILON)
return -1; // Ray parallel to triangle
// f = 1/a — we'll defer the division by working with a as denominator
// s = O - v0
int32_t sx = ox - tri.v0x;
int32_t sy = oy - tri.v0y;
int32_t sz = oz - tri.v0z;
// u = f * dot(s, h) = dot(s, h) / a
int32_t sh = dot3(sx, sy, sz, hx, hy, hz);
// Check u in [0, 1]: sh/a must be in [0, a] if a > 0, or [a, 0] if a < 0
if (a > 0) {
if (sh < 0 || sh > a) return -1;
} else {
if (sh > 0 || sh < a) return -1;
}
// q = cross(s, e1)
int32_t qx, qy, qz;
cross3(sx, sy, sz, tri.e1x, tri.e1y, tri.e1z, qx, qy, qz);
// v = f * dot(D, q) = dot(D, q) / a
int32_t dq = dot3(dx, dy, dz, qx, qy, qz);
if (a > 0) {
if (dq < 0 || sh + dq > a) return -1;
} else {
if (dq > 0 || sh + dq < a) return -1;
}
// t = f * dot(e2, q) = dot(e2, q) / a
int32_t eq = dot3(tri.e2x, tri.e2y, tri.e2z, qx, qy, qz);
// t in 20.12 using 32-bit division only
int32_t t = fpdiv(eq, a);
if (t < COLLISION_EPSILON) return -1; // Behind ray origin
return t;
}
// ============================================================================
// High-level: moveAndSlide
// ============================================================================
psyqo::Vec3 WorldCollision::moveAndSlide(const psyqo::Vec3& oldPos,
const psyqo::Vec3& newPos,
int32_t radius,
uint8_t currentRoom) const {
if (!isLoaded()) return newPos;
int32_t posX = newPos.x.raw();
int32_t posY = newPos.y.raw();
int32_t posZ = newPos.z.raw();
// Gather candidate meshes
uint16_t meshIndices[32];
int meshCount = gatherCandidateMeshes(posX, posZ, currentRoom, meshIndices, 32);
// Sphere AABB for broad phase
int32_t sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ;
sphereToAABB(posX, posY, posZ, radius + COLLISION_EPSILON,
sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ);
int triTests = 0;
int totalCollisions = 0;
for (int iter = 0; iter < MAX_COLLISION_ITERATIONS; iter++) {
bool collided = false;
for (int mi = 0; mi < meshCount && triTests < MAX_TRI_TESTS_PER_FRAME; mi++) {
const auto& mesh = m_meshes[meshIndices[mi]];
// Broad phase: sphere AABB vs mesh AABB
if (!aabbOverlap(sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ,
mesh.aabbMinX, mesh.aabbMinY, mesh.aabbMinZ,
mesh.aabbMaxX, mesh.aabbMaxY, mesh.aabbMaxZ)) {
continue;
}
for (int ti = 0; ti < mesh.triangleCount && triTests < MAX_TRI_TESTS_PER_FRAME; ti++) {
const auto& tri = m_triangles[mesh.firstTriangle + ti];
triTests++;
// Skip trigger surfaces
if (tri.flags & SURFACE_TRIGGER) continue;
// Skip floor and ceiling triangles — Y is resolved by nav regions.
// In PS1 space (Y-down): floor normals have ny < 0, ceiling ny > 0.
// If |ny| > walkable slope threshold, it's a floor/ceiling, not a wall.
int32_t absNy = tri.ny >= 0 ? tri.ny : -tri.ny;
if (absNy > WALKABLE_SLOPE_COS) continue;
int32_t nx, ny, nz;
int32_t pen = sphereVsTriangle(posX, posY, posZ, radius, tri, nx, ny, nz);
if (pen > 0) {
totalCollisions++;
// Push out along normal
posX += fpmul(pen + COLLISION_EPSILON, nx);
posY += fpmul(pen + COLLISION_EPSILON, ny);
posZ += fpmul(pen + COLLISION_EPSILON, nz);
// Update sphere AABB
sphereToAABB(posX, posY, posZ, radius + COLLISION_EPSILON,
sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ);
collided = true;
}
}
}
if (!collided) break;
}
psyqo::Vec3 result;
result.x.value = posX;
result.y.value = posY;
result.z.value = posZ;
return result;
}
// ============================================================================
// High-level: groundTrace
// ============================================================================
bool WorldCollision::groundTrace(const psyqo::Vec3& pos,
int32_t maxDist,
int32_t& groundY,
int32_t& groundNormalY,
uint8_t& surfaceFlags,
uint8_t currentRoom) const {
if (!isLoaded()) return false;
int32_t ox = pos.x.raw();
int32_t oy = pos.y.raw();
int32_t oz = pos.z.raw();
// Ray direction: straight down (positive Y in PS1 space = down)
int32_t dx = 0, dy = FP_ONE, dz = 0;
uint16_t meshIndices[32];
int meshCount = gatherCandidateMeshes(ox, oz, currentRoom, meshIndices, 32);
int32_t bestDist = maxDist;
bool hit = false;
for (int mi = 0; mi < meshCount; mi++) {
const auto& mesh = m_meshes[meshIndices[mi]];
// Quick reject: check if mesh is below us
if (mesh.aabbMinY > oy + maxDist) continue;
if (mesh.aabbMaxY < oy) continue;
if (ox < mesh.aabbMinX || ox > mesh.aabbMaxX) continue;
if (oz < mesh.aabbMinZ || oz > mesh.aabbMaxZ) continue;
for (int ti = 0; ti < mesh.triangleCount; ti++) {
const auto& tri = m_triangles[mesh.firstTriangle + ti];
if (tri.flags & SURFACE_TRIGGER) continue;
int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri);
if (t >= 0 && t < bestDist) {
bestDist = t;
groundY = oy + t; // Hit point Y
groundNormalY = tri.ny;
surfaceFlags = tri.flags;
hit = true;
}
}
}
return hit;
}
// ============================================================================
// High-level: ceilingTrace
// ============================================================================
bool WorldCollision::ceilingTrace(const psyqo::Vec3& pos,
int32_t playerHeight,
int32_t& ceilingY,
uint8_t currentRoom) const {
if (!isLoaded()) return false;
int32_t ox = pos.x.raw();
int32_t oy = pos.y.raw();
int32_t oz = pos.z.raw();
// Ray direction: straight up (negative Y in PS1 space)
int32_t dx = 0, dy = -FP_ONE, dz = 0;
uint16_t meshIndices[32];
int meshCount = gatherCandidateMeshes(ox, oz, currentRoom, meshIndices, 32);
int32_t bestDist = playerHeight;
bool hit = false;
for (int mi = 0; mi < meshCount; mi++) {
const auto& mesh = m_meshes[meshIndices[mi]];
if (mesh.aabbMaxY > oy) continue;
if (mesh.aabbMinY < oy - playerHeight) continue;
if (ox < mesh.aabbMinX || ox > mesh.aabbMaxX) continue;
if (oz < mesh.aabbMinZ || oz > mesh.aabbMaxZ) continue;
for (int ti = 0; ti < mesh.triangleCount; ti++) {
const auto& tri = m_triangles[mesh.firstTriangle + ti];
if (tri.flags & SURFACE_TRIGGER) continue;
int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri);
if (t >= 0 && t < bestDist) {
bestDist = t;
ceilingY = oy - t;
hit = true;
}
}
}
return hit;
}
// ============================================================================
// High-level: raycast (general purpose)
// ============================================================================
bool WorldCollision::raycast(int32_t ox, int32_t oy, int32_t oz,
int32_t dx, int32_t dy, int32_t dz,
int32_t maxDist,
CollisionHit& hit,
uint8_t currentRoom) const {
if (!isLoaded()) return false;
uint16_t meshIndices[32];
int meshCount = gatherCandidateMeshes(ox, oz, currentRoom, meshIndices, 32);
int32_t bestDist = maxDist;
bool found = false;
for (int mi = 0; mi < meshCount; mi++) {
const auto& mesh = m_meshes[meshIndices[mi]];
for (uint16_t ti = 0; ti < mesh.triangleCount; ti++) {
uint16_t triIdx = mesh.firstTriangle + ti;
const auto& tri = m_triangles[triIdx];
int32_t t = rayVsTriangle(ox, oy, oz, dx, dy, dz, tri);
if (t >= 0 && t < bestDist) {
bestDist = t;
hit.pointX = ox + fpmul(t, dx);
hit.pointY = oy + fpmul(t, dy);
hit.pointZ = oz + fpmul(t, dz);
hit.normalX = tri.nx;
hit.normalY = tri.ny;
hit.normalZ = tri.nz;
hit.distance = t;
hit.triangleIndex = triIdx;
hit.surfaceFlags = tri.flags;
found = true;
}
}
}
return found;
}
} // namespace psxsplash

222
src/worldcollision.hh Normal file
View File

@@ -0,0 +1,222 @@
#pragma once
/**
* worldcollision.hh - Player-vs-World Triangle Collision
*
* Architecture:
* 1. Broad phase: per-mesh AABB reject, then spatial grid (exterior) or
* room membership (interior) to narrow candidate meshes.
* 2. Narrow phase: per-triangle capsule-vs-triangle sweep.
* 3. Response: sliding projection along collision plane.
*
* All math is fixed-point 20.12. Zero floats. Deterministic at any framerate.
*/
#include <stdint.h>
#include <psyqo/fixed-point.hh>
#include <psyqo/vector.hh>
namespace psxsplash {
// ============================================================================
// Surface flags — packed per-triangle, exported from SplashEdit
// ============================================================================
enum SurfaceFlag : uint8_t {
SURFACE_SOLID = 0x01, // Normal solid wall/floor
SURFACE_SLOPE = 0x02, // Steep slope (treated as wall for movement)
SURFACE_STAIRS = 0x04, // Staircase (smooth Y interpolation)
SURFACE_TRIGGER = 0x08, // Non-solid trigger volume
SURFACE_NO_WALK = 0x10, // Marks geometry as non-walkable floor
};
// ============================================================================
// Collision triangle — world-space, pre-transformed, contiguous in memory
// 40 bytes each — v0(12) + v1(12) + v2(12) + normal(12) omitted to save
// Actually: 40 bytes = v0(12) + edge1(12) + edge2(12) + flags(1) + pad(3)
// We store edges for Moller-Trumbore intersection
// ============================================================================
struct CollisionTri {
// Vertex 0 (world-space 20.12 fixed-point)
int32_t v0x, v0y, v0z; // 12 bytes
// Edge1 = v1 - v0
int32_t e1x, e1y, e1z; // 12 bytes
// Edge2 = v2 - v0
int32_t e2x, e2y, e2z; // 12 bytes
// Precomputed face normal (unit-ish, 20.12)
int32_t nx, ny, nz; // 12 bytes
// Surface properties
uint8_t flags; // SurfaceFlag bitmask
uint8_t roomIndex; // Room/chunk this tri belongs to (0xFF = none)
uint16_t pad; // Alignment
};
static_assert(sizeof(CollisionTri) == 52, "CollisionTri must be 52 bytes");
// ============================================================================
// Collision mesh header — one per collision mesh in the splashpack
// The triangles themselves follow contiguously after all headers.
// ============================================================================
struct CollisionMeshHeader {
// World-space AABB for broad-phase rejection (20.12 fixed-point)
int32_t aabbMinX, aabbMinY, aabbMinZ; // 12 bytes
int32_t aabbMaxX, aabbMaxY, aabbMaxZ; // 12 bytes
// Offset into the collision triangle array
uint16_t firstTriangle; // Index of first CollisionTri
uint16_t triangleCount; // Number of triangles
// Room/chunk association
uint8_t roomIndex; // Interior room index (0xFF = exterior)
uint8_t pad[3];
};
static_assert(sizeof(CollisionMeshHeader) == 32, "CollisionMeshHeader must be 32 bytes");
// ============================================================================
// Spatial chunk for exterior scenes — 2D grid over XZ
// ============================================================================
struct CollisionChunk {
uint16_t firstMeshIndex; // Index into CollisionMeshHeader array
uint16_t meshCount; // Number of meshes in this chunk
};
static_assert(sizeof(CollisionChunk) == 4, "CollisionChunk must be 4 bytes");
// ============================================================================
// Collision data header — describes the entire collision dataset
// ============================================================================
struct CollisionDataHeader {
uint16_t meshCount; // Number of CollisionMeshHeader entries
uint16_t triangleCount; // Total CollisionTri entries
uint16_t chunkGridW; // Spatial grid width (0 if interior)
uint16_t chunkGridH; // Spatial grid height (0 if interior)
int32_t chunkOriginX; // Grid origin X (20.12)
int32_t chunkOriginZ; // Grid origin Z (20.12)
int32_t chunkSize; // Cell size (20.12)
// Total: 20 bytes
// Followed by: meshCount * CollisionMeshHeader
// triangleCount * CollisionTri
// chunkGridW * chunkGridH * CollisionChunk (if exterior)
};
static_assert(sizeof(CollisionDataHeader) == 20, "CollisionDataHeader must be 20 bytes");
// ============================================================================
// Hit result from collision queries
// ============================================================================
struct CollisionHit {
int32_t pointX, pointY, pointZ; // Hit point (20.12)
int32_t normalX, normalY, normalZ; // Hit normal (20.12)
int32_t distance; // Distance along ray (20.12)
uint16_t triangleIndex; // Which triangle was hit
uint8_t surfaceFlags; // SurfaceFlag of hit triangle
uint8_t pad;
};
// ============================================================================
// Maximum slope angle for walkable surfaces
// cos(46°) ≈ 0.6947 → in 20.12 fixed-point = 2845
// Surfaces with normal.y < this are treated as walls
// ============================================================================
static constexpr int32_t WALKABLE_SLOPE_COS = 2845; // cos(46°) in 20.12
// Player collision capsule radius (20.12 fixed-point)
// ~0.5 world units at GTEScaling=100 → 0.005 GTE units → 20 in 20.12
static constexpr int32_t PLAYER_RADIUS = 20;
// Small epsilon for collision (20.12)
// ≈ 0.01 GTE units
static constexpr int32_t COLLISION_EPSILON = 41;
// Maximum number of collision iterations per frame
static constexpr int MAX_COLLISION_ITERATIONS = 8;
// Maximum triangles to test per frame (budget)
static constexpr int MAX_TRI_TESTS_PER_FRAME = 256;
// ============================================================================
// WorldCollision — main collision query interface
// Loaded from splashpack data, used by SceneManager every frame
// ============================================================================
class WorldCollision {
public:
WorldCollision() = default;
/// Initialize from splashpack data. Returns pointer past the data.
const uint8_t* initializeFromData(const uint8_t* data);
/// Is collision data loaded?
bool isLoaded() const { return m_triangles != nullptr; }
// ========================================================================
// High-level queries used by the player movement system
// ========================================================================
/// Move a sphere from oldPos to newPos, sliding against world geometry.
/// Returns the final valid position after collision response.
/// radius is in 20.12 fixed-point.
psyqo::Vec3 moveAndSlide(const psyqo::Vec3& oldPos,
const psyqo::Vec3& newPos,
int32_t radius,
uint8_t currentRoom) const;
/// Cast a ray downward from pos to find the ground.
/// Returns true if ground found within maxDist.
/// groundY and groundNormal are filled on hit.
bool groundTrace(const psyqo::Vec3& pos,
int32_t maxDist,
int32_t& groundY,
int32_t& groundNormalY,
uint8_t& surfaceFlags,
uint8_t currentRoom) const;
/// Cast a ray upward to detect ceilings.
bool ceilingTrace(const psyqo::Vec3& pos,
int32_t playerHeight,
int32_t& ceilingY,
uint8_t currentRoom) const;
/// Raycast against collision geometry. Returns true on hit.
bool raycast(int32_t ox, int32_t oy, int32_t oz,
int32_t dx, int32_t dy, int32_t dz,
int32_t maxDist,
CollisionHit& hit,
uint8_t currentRoom) const;
/// Get mesh count for debugging
uint16_t getMeshCount() const { return m_header.meshCount; }
uint16_t getTriangleCount() const { return m_header.triangleCount; }
private:
CollisionDataHeader m_header = {};
const CollisionMeshHeader* m_meshes = nullptr;
const CollisionTri* m_triangles = nullptr;
const CollisionChunk* m_chunks = nullptr; // Only for exterior scenes
/// Collect candidate mesh indices near a position.
/// For exterior: uses spatial grid. For interior: uses roomIndex.
int gatherCandidateMeshes(int32_t posX, int32_t posZ,
uint8_t currentRoom,
uint16_t* outIndices,
int maxIndices) const;
/// Test a sphere against a single triangle. Returns penetration depth (>0 if colliding).
/// On collision, fills outNormal with the push-out direction.
int32_t sphereVsTriangle(int32_t cx, int32_t cy, int32_t cz,
int32_t radius,
const CollisionTri& tri,
int32_t& outNx, int32_t& outNy, int32_t& outNz) const;
/// Ray vs triangle (Moller-Trumbore in fixed-point).
/// Returns distance along ray (20.12), or -1 if no hit.
int32_t rayVsTriangle(int32_t ox, int32_t oy, int32_t oz,
int32_t dx, int32_t dy, int32_t dz,
const CollisionTri& tri) const;
/// AABB vs AABB test
static bool aabbOverlap(int32_t aMinX, int32_t aMinY, int32_t aMinZ,
int32_t aMaxX, int32_t aMaxY, int32_t aMaxZ,
int32_t bMinX, int32_t bMinY, int32_t bMinZ,
int32_t bMaxX, int32_t bMaxY, int32_t bMaxZ);
/// Expand a point to an AABB with radius
static void sphereToAABB(int32_t cx, int32_t cy, int32_t cz, int32_t r,
int32_t& minX, int32_t& minY, int32_t& minZ,
int32_t& maxX, int32_t& maxY, int32_t& maxZ);
};
} // namespace psxsplash

BIN
test_literal.cpp Normal file

Binary file not shown.