Broken runtime

This commit is contained in:
Jan Racek
2026-03-27 13:46:42 +01:00
parent eacbf4de46
commit 090402f71a
25 changed files with 1111 additions and 877 deletions

View File

@@ -14,6 +14,7 @@ src/lua.cpp \
src/luaapi.cpp \
src/scenemanager.cpp \
src/sceneloader.cpp \
src/fileloader.cpp \
src/audiomanager.cpp \
src/controls.cpp \
src/profiler.cpp \
@@ -23,7 +24,13 @@ src/cutscene.cpp \
src/uisystem.cpp \
src/loadingscreen.cpp
# LOADER=cdrom → CD-ROM backend (for ISO builds on real hardware)
# LOADER=pcdrv → PCdrv backend (default, emulator + SIO1)
ifeq ($(LOADER),cdrom)
CPPFLAGS += -DLOADER_CDROM
else
CPPFLAGS += -DPCDRV_SUPPORT=1
endif
include third_party/nugget/psyqo-lua/psyqo-lua.mk
include third_party/nugget/psyqo/psyqo.mk

View File

@@ -8,20 +8,14 @@
namespace psxsplash {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
uint16_t AudioManager::volToHw(int v) {
if (v <= 0) return 0;
if (v >= 128) return 0x3fff;
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();
@@ -40,26 +34,24 @@ void AudioManager::reset() {
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;
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.
// check for and skip VAG header if present
if (sizeBytes >= 48) {
const char* magic = reinterpret_cast<const char*>(adpcmData);
if (magic[0] == 'V' && magic[1] == 'A' && magic[2] == 'G' && magic[3] == 'p') {
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;
@@ -67,46 +59,26 @@ bool AudioManager::loadClip(int clipIndex, const uint8_t* adpcmData, uint32_t si
return false;
}
// psyqo::SPU::dmaWrite takes dataSize as uint16_t and uses blockSize=4:
// BCR = blockSize | ((dataSize / blockSize) << 16)
// block_count = dataSize / blockSize (integer division - truncates!)
// actual bytes = block_count * blockSize * 4
//
// With blockSize=4: each block = 16 bytes. Max block_count that fits
// in uint16_t's BCR field: 4095. Max clean transfer: 4095 * 16 = 65520.
// bytesThisRound MUST be a multiple of 16 to avoid the integer division
// truncation causing fewer bytes to be DMA'd than the pointer advances.
const uint8_t* src = adpcmData;
const uint8_t *src = adpcmData;
uint32_t remaining = alignedSize;
uint32_t dstAddr = addr;
while (remaining > 0) {
// Max transfer per call: 65520 bytes (4095 blocks * 16 bytes each).
uint32_t bytesThisRound = (remaining > 65520u) ? 65520u : remaining;
bytesThisRound &= ~15u; // 16-byte block alignment
if (bytesThisRound == 0) break;
if (bytesThisRound == 0)
break;
uint16_t dmaSizeParam = (uint16_t)(bytesThisRound / 4);
psyqo::SPU::dmaWrite(dstAddr, src, dmaSizeParam, 4);
// PSYQo's internal waitForStatus only spins ~10000 iterations (~1.8ms).
// On real hardware, SPU DMA for 65KB takes tens of milliseconds.
// The timeout fires, the function returns, and the next chunk starts
// while the previous transfer is still in progress - corrupting data.
// Spin here until the DMA controller's busy bit actually clears.
while (DMA_CTRL[DMA_SPU].CHCR & (1 << 24)) {}
while (DMA_CTRL[DMA_SPU].CHCR & (1 << 24)) {
}
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.
// Restore SPU to manual (non-DMA) mode after upload.
// psyqo::SPU::dmaWrite sets SPU_CTRL bit 5 (DMA write mode) but never
// clears it. On real hardware, voice register writes (pitch, volume, etc.)
// may be ignored while the SPU bus is still in DMA mode.
SPU_CTRL &= ~(0b11 << 4);
m_clips[clipIndex].spuAddr = addr;
@@ -119,19 +91,17 @@ bool AudioManager::loadClip(int clipIndex, const uint8_t* adpcmData, uint32_t si
return true;
}
// ---------------------------------------------------------------------------
// Playback
// ---------------------------------------------------------------------------
int AudioManager::play(int clipIndex, int volume, int pan) {
if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS || !m_clips[clipIndex].loaded) {
if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS ||
!m_clips[clipIndex].loaded) {
return -1;
}
uint32_t ch = psyqo::SPU::getNextFreeChannel();
if (ch == psyqo::SPU::NO_FREE_CHANNEL) return -1;
if (ch == psyqo::SPU::NO_FREE_CHANNEL)
return -1;
const AudioClip& clip = m_clips[clipIndex];
const AudioClip &clip = m_clips[clipIndex];
uint16_t vol = volToHw(volume);
uint16_t leftVol = vol;
@@ -142,10 +112,6 @@ int AudioManager::play(int clipIndex, int volume, int pan) {
rightVol = (uint16_t)((uint32_t)vol * p / 127);
}
// Set the repeat address depending on loop mode.
// 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);
@@ -153,19 +119,13 @@ int AudioManager::play(int clipIndex, int volume, int pan) {
SPU_VOICES[ch].sampleRepeatAddr = DUMMY_SPU_ADDR / 8;
}
// Build playback config
psyqo::SPU::ChannelPlaybackConfig config;
config.sampleRate.value = static_cast<uint16_t>(((uint32_t)clip.sampleRate << 12) / 44100);
config.sampleRate.value =
static_cast<uint16_t>(((uint32_t)clip.sampleRate << 12) / 44100);
config.volumeLeft = leftVol;
config.volumeRight = rightVol;
config.adsr = DEFAULT_ADSR;
// Write SPU voice registers directly instead of PSYQo's playADPCM(),
// which truncates addresses above 64KB (uint16_t parameter).
// The sampleStartAddr register stores addr/8, so uint16_t covers
// the full 512KB SPU RAM range.
// KEY_OFF (hard cut)
if (ch > 15) {
SPU_KEY_OFF_HIGH = 1 << (ch - 16);
} else {
@@ -179,7 +139,6 @@ int AudioManager::play(int clipIndex, int volume, int pan) {
SPU_VOICES[ch].ad = config.adsr & 0xFFFF;
SPU_VOICES[ch].sr = (config.adsr >> 16) & 0xFFFF;
// KEY_ON
if (ch > 15) {
SPU_KEY_ON_HIGH = 1 << (ch - 16);
} else {
@@ -189,44 +148,34 @@ int AudioManager::play(int clipIndex, int volume, int pan) {
return static_cast<int>(ch);
}
// ---------------------------------------------------------------------------
// Stop
// ---------------------------------------------------------------------------
void AudioManager::stopVoice(int channel) {
if (channel < 0 || channel >= MAX_VOICES) return;
if (channel < 0 || channel >= MAX_VOICES)
return;
psyqo::SPU::silenceChannels(1u << channel);
}
void AudioManager::stopAll() {
psyqo::SPU::silenceChannels(0x00FFFFFFu);
}
// ---------------------------------------------------------------------------
// Volume
// ---------------------------------------------------------------------------
void AudioManager::stopAll() { psyqo::SPU::silenceChannels(0x00FFFFFFu); }
void AudioManager::setVoiceVolume(int channel, int volume, int pan) {
if (channel < 0 || channel >= MAX_VOICES) return;
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].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++;
if (m_clips[i].loaded)
count++;
}
return count;
}

View File

@@ -4,46 +4,25 @@
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.
///
/// Note: psyqo::SPU::playADPCM() takes a uint16_t for the address,
/// which would limit to 64KB. We bypass it and write SPU registers
/// directly to address the full 512KB range (register stores addr/8,
/// so uint16_t covers 0-0x7FFF8).
static constexpr uint32_t SPU_RAM_START = 0x1010;
static constexpr uint32_t SPU_RAM_END = 0x80000;
/// Default ADSR: instant attack, sustain at max, ~46ms linear release.
/// Lower 16-bit (AD): attack linear shift=0 step=0("+7"), decay shift=0,
/// sustain level=0xF (max -> decay skipped)
/// Upper 16-bit (SR): sustain linear increase shift=0 step=0("+7"),
/// release linear shift=10 (~46ms to zero)
static constexpr uint32_t DEFAULT_ADSR = 0x000A000F;
/// 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
uint32_t spuAddr;
uint32_t size;
uint16_t sampleRate;
bool loop;
bool loaded;
};
/// 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
@@ -59,15 +38,15 @@ public:
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,
/// Data must be 16-byte aligned. Returns true on success.
bool loadClip(int clipIndex, const uint8_t *adpcmData, uint32_t sizeBytes,
uint16_t sampleRate, bool loop);
/// Play a clip by index. Returns channel (0-23), or -1 if full.
/// Volume: 0-128 (128=max). Pan: 0 (left) to 127 (right), 64 = center.
int play(int clipIndex, int volume = 128, int pan = 64);
/// Stop a specific channel (returned from play())
/// Stop a specific channel
void stopVoice(int channel);
/// Stop all playing channels
@@ -76,7 +55,7 @@ public:
/// 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)
/// Get total SPU RAM used by loaded clips
uint32_t getUsedSPURam() const { return m_nextAddr - SPU_RAM_START; }
/// Get total SPU RAM available
@@ -85,7 +64,7 @@ public:
/// Get number of loaded clips
int getLoadedClipCount() const;
/// Reset all clips and free SPU RAM (call on scene unload)
/// Reset all clips and free SPU RAM
void reset();
private:
@@ -93,7 +72,7 @@ private:
static uint16_t volToHw(int v);
AudioClip m_clips[MAX_AUDIO_CLIPS];
uint32_t m_nextAddr = SPU_RAM_START; // Bump allocator for SPU RAM
uint32_t m_nextAddr = SPU_RAM_START;
};
} // namespace psxsplash

View File

@@ -2,64 +2,43 @@
namespace psxsplash {
void BVHManager::initialize(const BVHNode* nodes, uint16_t nodeCount,
const TriangleRef* triangleRefs, uint16_t triangleRefCount) {
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 BVHManager::cullFrustum(const Frustum &frustum, TriangleRef *outRefs,
int maxRefs) const {
if (!isLoaded() || m_nodeCount == 0) return 0;
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 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;
if (nodeIndex < 0 || nodeIndex >= m_nodeCount)
return currentCount;
if (currentCount >= maxRefs)
return currentCount;
const BVHNode& node = m_nodes[nodeIndex];
const BVHNode &node = m_nodes[nodeIndex];
// Frustum test - if node is completely outside, skip entire subtree
if (!frustum.testAABB(node)) {
return currentCount; // Culled!
return currentCount;
}
// If leaf, add all triangles
if (node.isLeaf()) {
int count = node.triangleCount;
int available = maxRefs - currentCount;
if (count > available) count = available;
if (count > available)
count = available;
for (int i = 0; i < count; i++) {
outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i];
@@ -67,12 +46,13 @@ int BVHManager::traverseFrustum(int nodeIndex,
return currentCount + count;
}
// Recurse into children
if (node.leftChild != 0xFFFF) {
currentCount = traverseFrustum(node.leftChild, frustum, outRefs, currentCount, maxRefs);
currentCount = traverseFrustum(node.leftChild, frustum, outRefs,
currentCount, maxRefs);
}
if (node.rightChild != 0xFFFF) {
currentCount = traverseFrustum(node.rightChild, frustum, outRefs, currentCount, maxRefs);
currentCount = traverseFrustum(node.rightChild, frustum, outRefs,
currentCount, maxRefs);
}
return currentCount;
@@ -80,34 +60,33 @@ int BVHManager::traverseFrustum(int nodeIndex,
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;
TriangleRef *outRefs, int maxRefs) const {
if (!isLoaded() || m_nodeCount == 0)
return 0;
return traverseRegion(0, minX, minY, minZ, maxX, maxY, maxZ, outRefs, 0, maxRefs);
return traverseRegion(0, minX, minY, minZ, maxX, maxY, maxZ, outRefs, 0,
maxRefs);
}
int BVHManager::traverseRegion(int nodeIndex,
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;
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];
const BVHNode &node = m_nodes[nodeIndex];
// AABB overlap test
if (!aabbOverlap(node, qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ)) {
return currentCount; // No overlap, skip
}
return currentCount;
// If leaf, add all triangles
if (node.isLeaf()) {
int count = node.triangleCount;
int available = maxRefs - currentCount;
if (count > available) count = available;
if (count > available)
count = available;
for (int i = 0; i < count; i++) {
outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i];
@@ -115,29 +94,31 @@ int BVHManager::traverseRegion(int nodeIndex,
return currentCount + count;
}
// Recurse into children
if (node.leftChild != 0xFFFF) {
currentCount = traverseRegion(node.leftChild,
qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ,
outRefs, currentCount, maxRefs);
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);
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
bool BVHManager::aabbOverlap(const BVHNode &node, int32_t qMinX, int32_t qMinY,
int32_t qMinZ, int32_t qMaxX, int32_t qMaxY,
int32_t qMaxZ) {
if (node.maxX < qMinX || node.minX > qMaxX)
return false;
if (node.maxY < qMinY || node.minY > qMaxY)
return false;
if (node.maxZ < qMinZ || node.minZ > qMaxZ)
return false;
return true;
}
} // namespace psxsplash

View File

@@ -14,42 +14,31 @@ struct TriangleRef {
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
int32_t minX, minY, minZ;
int32_t maxX, maxY, maxZ;
// Child indices (0xFFFF = no child / leaf indicator)
uint16_t leftChild; // 2 bytes
uint16_t rightChild; // 2 bytes
uint16_t leftChild;
uint16_t rightChild;
// Triangle data (only valid for leaf nodes)
uint16_t firstTriangle; // 2 bytes - index into triangle ref array
uint16_t triangleCount; // 2 bytes
uint16_t firstTriangle;
uint16_t triangleCount;
/// 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;
}
@@ -65,21 +54,18 @@ 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
Plane planes[6];
/// 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 false;
}
}
return true; // Potentially visible
return true;
}
};
@@ -90,10 +76,6 @@ public:
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
@@ -117,6 +99,11 @@ public:
/// Check if BVH is loaded
bool isLoaded() const { return m_nodes != nullptr; }
void relocate(intptr_t delta) {
if (m_nodes) m_nodes = reinterpret_cast<const BVHNode*>(reinterpret_cast<intptr_t>(m_nodes) + delta);
if (m_triangleRefs) m_triangleRefs = reinterpret_cast<const TriangleRef*>(reinterpret_cast<intptr_t>(m_triangleRefs) + delta);
}
private:
const BVHNode* m_nodes = nullptr;
const TriangleRef* m_triangleRefs = nullptr;

View File

@@ -6,7 +6,6 @@
#include <psyqo/trigonometry.hh>
psxsplash::Camera::Camera() {
// Load identity
m_rotationMatrix = psyqo::SoftMath::generateRotationMatrix33(0, psyqo::SoftMath::Axis::X, m_trig);
}
@@ -31,7 +30,6 @@ void psxsplash::Camera::SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle
auto rotY = psyqo::SoftMath::generateRotationMatrix33(y, psyqo::SoftMath::Axis::Y, m_trig);
auto rotZ = psyqo::SoftMath::generateRotationMatrix33(z, psyqo::SoftMath::Axis::Z, m_trig);
// XYZ multiplication order (matches C#)
psyqo::SoftMath::multiplyMatrix33(rotY, rotX, &rotY);
psyqo::SoftMath::multiplyMatrix33(rotY, rotZ, &rotY);
@@ -41,39 +39,11 @@ void psxsplash::Camera::SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle
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
constexpr int32_t SCREEN_HALF_WIDTH = 160;
constexpr int32_t SCREEN_HALF_HEIGHT = 120;
constexpr int32_t H = 120;
// 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();
@@ -90,41 +60,17 @@ void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
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
frustum.planes[1].d = fwdDotCam + (4096 * 2000);
// =========================================================================
// SIDE PLANES - Based on actual GTE FOV
//
// The frustum edge in camera space goes through (±screenHalf, 0, H).
// Plane normal (pointing INTO frustum) = right * H + forward * screenHalfWidth
// (for left plane, we add right; for right plane, we subtract right)
//
// Note: axes are in 4.12 fixed point (4096 = 1.0), but H and screen values
// are integers. We scale H to match: H * 4096 / some_factor
// Since we just need the ratio, we can use H and screenHalf directly
// as weights for the axis vectors.
// =========================================================================
// PLANE 2: LEFT PLANE - cull things to the LEFT of view
// Normal = right * H + forward * screenHalfWidth (points into frustum)
frustum.planes[2].nx = ((int64_t)rightX * H + (int64_t)fwdX * SCREEN_HALF_WIDTH) >> 12;
frustum.planes[2].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;
@@ -132,8 +78,6 @@ void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
(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;
@@ -141,8 +85,6 @@ void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
(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;
@@ -150,8 +92,6 @@ void psxsplash::Camera::ExtractFrustum(Frustum& frustum) const {
(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;

View File

@@ -8,7 +8,6 @@
namespace psxsplash {
// Camera class for managing 3D position and rotation.
class Camera {
public:
Camera();
@@ -23,11 +22,8 @@ class Camera {
void SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z);
psyqo::Matrix33& GetRotation();
/// Extract frustum planes for culling
/// Near/Far planes based on typical PS1 draw distances
void ExtractFrustum(Frustum& frustum) const;
/// Cached Euler angles (psyqo::Angle raw values) from the last SetRotation() call.
int16_t GetAngleX() const { return m_angleX; }
int16_t GetAngleY() const { return m_angleY; }
int16_t GetAngleZ() const { return m_angleZ; }

View File

@@ -156,32 +156,22 @@ void CollisionSystem::reset() {
void CollisionSystem::registerCollider(uint16_t gameObjectIndex, const AABB& localBounds,
CollisionType type, CollisionMask mask) {
if (m_colliderCount >= MAX_COLLIDERS) {
// Out of collider slots
return;
}
if (m_colliderCount >= MAX_COLLIDERS) return;
CollisionData& data = m_colliders[m_colliderCount++];
data.bounds = localBounds; // Will be transformed in updateCollider
data.localBounds = localBounds;
data.bounds = localBounds;
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;
m_colliders[i].bounds.min = m_colliders[i].localBounds.min + position;
m_colliders[i].bounds.max = m_colliders[i].localBounds.max + position;
break;
}
}

View File

@@ -1,17 +1,5 @@
#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>
@@ -20,29 +8,17 @@
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
Solid = 1,
Trigger = 2,
Platform = 3
};
/**
* 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;
@@ -61,7 +37,6 @@ struct AABB {
(point.z >= min.z && point.z <= max.z);
}
// Get center of AABB
psyqo::Vec3 center() const {
return psyqo::Vec3{
(min.x + max.x) / 2,
@@ -70,7 +45,6 @@ struct AABB {
};
}
// Get half-extents
psyqo::Vec3 halfExtents() const {
return psyqo::Vec3{
(max.x - min.x) / 2,
@@ -79,59 +53,39 @@ struct AABB {
};
}
// 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)");
static_assert(sizeof(AABB) == 24);
/**
* 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)
AABB localBounds;
AABB bounds;
CollisionType type;
CollisionMask layerMask;
uint16_t gameObjectIndex;
};
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
uint16_t objectA;
uint16_t objectB;
psyqo::Vec3 normal;
psyqo::FixedPoint<12> penetration;
};
/**
* 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
uint16_t triggerIndex;
uint16_t otherIndex;
uint8_t framesSinceContact;
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 GRID_SIZE = 8;
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;
@@ -142,30 +96,16 @@ public:
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;
@@ -174,65 +114,43 @@ public:
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);
};

46
src/fileloader.cpp Normal file
View File

@@ -0,0 +1,46 @@
#include "fileloader.hh"
#include <psyqo/xprintf.h>
// ── Backend selection ────────────────────────────────────────────
// LOADER_CDROM is defined by the Makefile when LOADER=cdrom.
// Default (including PCDRV_SUPPORT=1) selects the PCdrv backend.
#if defined(LOADER_CDROM)
#include "fileloader_cdrom.hh"
#else
#include "fileloader_pcdrv.hh"
#endif
namespace psxsplash {
// ── Singleton ────────────────────────────────────────────────────
FileLoader& FileLoader::Get() {
#if defined(LOADER_CDROM)
static FileLoaderCDRom instance;
#else
static FileLoaderPCdrv instance;
#endif
return instance;
}
// ── Filename helpers ─────────────────────────────────────────────
// PCdrv uses lowercase names matching the files SplashControlPanel
// writes to PSXBuild/. CDRom uses uppercase 8.3 ISO9660 names with
// the mandatory ";1" version suffix.
void FileLoader::BuildSceneFilename(int sceneIndex, char* out, int maxLen) {
#if defined(LOADER_CDROM)
snprintf(out, maxLen, "SCENE_%d.SPK;1", sceneIndex);
#else
snprintf(out, maxLen, "scene_%d.splashpack", sceneIndex);
#endif
}
void FileLoader::BuildLoadingFilename(int sceneIndex, char* out, int maxLen) {
#if defined(LOADER_CDROM)
snprintf(out, maxLen, "SCENE_%d.LDG;1", sceneIndex);
#else
snprintf(out, maxLen, "scene_%d.loading", sceneIndex);
#endif
}
} // namespace psxsplash

116
src/fileloader.hh Normal file
View File

@@ -0,0 +1,116 @@
#pragma once
#include <stdint.h>
#include <psyqo/task.hh>
namespace psxsplash {
/**
* FileLoader — abstract interface for loading files on PS1.
*
* Two compile-time backends:
* - FileLoaderPCdrv: PCdrv protocol (emulator break instructions OR SIO1 serial)
* - FileLoaderCDRom: CD-ROM via psyqo CDRomDevice + ISO9660Parser
*
* Build with LOADER=pcdrv (default) or LOADER=cdrom to select the backend.
*
* Both backends expose the same task-based API following the psyqo TaskQueue
* pattern (see nugget/psyqo/examples/task-demo). For PCdrv the tasks resolve
* synchronously; for CD-ROM they chain real async hardware I/O.
*
* The active backend singleton is accessed through FileLoader::Get().
*/
class FileLoader {
public:
virtual ~FileLoader() = default;
/**
* Called once from Application::prepare() before any GPU work.
* CDRom backend uses this to call CDRomDevice::prepare().
* PCdrv backend is a no-op.
*/
virtual void prepare() {}
/**
* Returns a Task that initialises the loader.
*
* PCdrv: calls pcdrv_sio1_init() + pcdrv_init(), resolves immediately.
* CDRom: chains CDRomDevice::scheduleReset + ISO9660Parser::scheduleInitialize.
*/
virtual psyqo::TaskQueue::Task scheduleInit() = 0;
/**
* Returns a Task that loads a file.
*
* On resolve, *outBuffer points to the loaded data (caller owns it)
* and *outSize contains the size in bytes.
* On reject, *outBuffer == nullptr and *outSize == 0.
*
* PCdrv filenames: relative paths like "scene_0.splashpack".
* CDRom filenames: ISO9660 names like "SCENE_0.SPK;1".
*
* Use BuildSceneFilename / BuildLoadingFilename helpers to get the
* correct filename for the active backend.
*/
virtual psyqo::TaskQueue::Task scheduleLoadFile(
const char* filename, uint8_t*& outBuffer, int& outSize) = 0;
/**
* Synchronously loads a file. Provided for call sites that cannot
* easily be converted to task chains (e.g. SceneManager scene transitions).
*
* CDRom backend: uses blocking readSectorsBlocking via GPU spin-loop.
* PCdrv backend: same as the sync pcdrv_open/read/close flow.
*
* Returns loaded data (caller-owned), or nullptr on failure.
*/
virtual uint8_t* LoadFileSync(const char* filename, int& outSize) = 0;
/**
* Optional progress-reporting variant of LoadFileSync.
*
* @param progress If non-null, the backend may call progress->fn()
* periodically during the load with interpolated
* percentage values between startPercent and endPercent.
*
* Default implementation delegates to LoadFileSync and calls the
* callback once at endPercent. CDRom backend overrides this to
* read in 64 KB chunks and report after each chunk.
*/
struct LoadProgressInfo {
void (*fn)(uint8_t percent, void* userData);
void* userData;
uint8_t startPercent;
uint8_t endPercent;
};
virtual uint8_t* LoadFileSyncWithProgress(
const char* filename, int& outSize,
const LoadProgressInfo* progress)
{
auto* data = LoadFileSync(filename, outSize);
if (progress && progress->fn)
progress->fn(progress->endPercent, progress->userData);
return data;
}
/** Free a buffer returned by scheduleLoadFile or LoadFileSync. */
virtual void FreeFile(uint8_t* data) = 0;
/** Human-readable name for logging ("pcdrv" / "cdrom"). */
virtual const char* Name() const = 0;
// ── Filename helpers ──────────────────────────────────────────
// Build the correct filename for the active backend.
/** scene_N.splashpack or SCENE_N.SPK;1 */
static void BuildSceneFilename(int sceneIndex, char* out, int maxLen);
/** scene_N.loading or SCENE_N.LDG;1 */
static void BuildLoadingFilename(int sceneIndex, char* out, int maxLen);
// ── Singleton ─────────────────────────────────────────────────
static FileLoader& Get();
};
} // namespace psxsplash

234
src/fileloader_cdrom.hh Normal file
View File

@@ -0,0 +1,234 @@
#pragma once
#include "fileloader.hh"
#include <psyqo/cdrom-device.hh>
#include <psyqo/iso9660-parser.hh>
#include <psyqo/gpu.hh>
#include <psyqo/task.hh>
namespace psxsplash {
/**
* FileLoaderCDRom — loads files from CD-ROM using the psyqo ISO9660 parser.
*
* Follows the same pattern as nugget/psyqo/examples/task-demo:
* 1. CDRomDevice::prepare() — called from Application::prepare()
* 2. CDRomDevice::scheduleReset() — reset the drive
* 3. ISO9660Parser::scheduleInitialize() — parse the PVD and root dir
* 4. ISO9660Parser::scheduleGetDirentry — look up a file by path
* 5. ISO9660Parser::scheduleReadRequest — read the file sectors
*
* The BIOS has already read SYSTEM.CNF and launched the executable by the
* time we get here, so we don't touch SYSTEM.CNF at all.
*/
class FileLoaderCDRom final : public FileLoader {
public:
FileLoaderCDRom() : m_isoParser(&m_cdrom) {}
// ── prepare: must be called from Application::prepare() ──────
void prepare() override {
m_cdrom.prepare();
}
// ── scheduleInit ─────────────────────────────────────────────
// Chains: reset CD drive → initialise ISO9660 parser.
psyqo::TaskQueue::Task scheduleInit() override {
m_initQueue
.startWith(m_cdrom.scheduleReset())
.then(m_isoParser.scheduleInitialize());
return m_initQueue.schedule();
}
// ── scheduleLoadFile ─────────────────────────────────────────
// Chains: getDirentry → allocate + read.
// The lambda captures filename/outBuffer/outSize by reference;
// they must remain valid until the owning TaskQueue completes.
psyqo::TaskQueue::Task scheduleLoadFile(
const char* filename, uint8_t*& outBuffer, int& outSize) override
{
return psyqo::TaskQueue::Task(
[this, filename, &outBuffer, &outSize](psyqo::TaskQueue::Task* task) {
// Stash the parent task so callbacks can resolve/reject it.
m_pendingTask = task;
m_pOutBuffer = &outBuffer;
m_pOutSize = &outSize;
outBuffer = nullptr;
outSize = 0;
// Step 1 — look up the directory entry.
m_isoParser.getDirentry(
filename, &m_request.entry,
[this](bool success) {
if (!success ||
m_request.entry.type ==
psyqo::ISO9660Parser::DirEntry::INVALID) {
m_pendingTask->reject();
return;
}
// Step 2 — allocate a sector-aligned buffer and read.
uint32_t sectors =
(m_request.entry.size + 2047) / 2048;
uint8_t* buf = new uint8_t[sectors * 2048];
*m_pOutBuffer = buf;
*m_pOutSize = static_cast<int>(m_request.entry.size);
m_request.buffer = buf;
// Step 3 — chain the actual CD read via a sub-queue.
m_readQueue
.startWith(
m_isoParser.scheduleReadRequest(&m_request))
.then([this](psyqo::TaskQueue::Task* inner) {
// Read complete — resolve the outer task.
m_pendingTask->resolve();
inner->resolve();
})
.butCatch([this](psyqo::TaskQueue*) {
// Read failed — clean up and reject.
delete[] *m_pOutBuffer;
*m_pOutBuffer = nullptr;
*m_pOutSize = 0;
m_pendingTask->reject();
})
.run();
});
});
}
// ── LoadFileSyncWithProgress ───────────────────────────────
// Reads the file in 32-sector (64 KB) chunks, calling the
// progress callback between each chunk so the loading bar
// animates during the (slow) CD-ROM transfer.
uint8_t* LoadFileSyncWithProgress(
const char* filename, int& outSize,
const LoadProgressInfo* progress) override
{
outSize = 0;
if (!m_isoParser.initialized()) return nullptr;
// --- getDirentry (blocking via one-shot TaskQueue) ---
psyqo::ISO9660Parser::DirEntry entry;
bool found = false;
m_syncQueue
.startWith(m_isoParser.scheduleGetDirentry(filename, &entry))
.then([&found](psyqo::TaskQueue::Task* t) {
found = true;
t->resolve();
})
.butCatch([](psyqo::TaskQueue*) {})
.run();
while (m_syncQueue.isRunning()) {
m_gpu->pumpCallbacks();
}
if (!found || entry.type == psyqo::ISO9660Parser::DirEntry::INVALID)
return nullptr;
// --- chunked sector read with progress ---
uint32_t totalSectors = (entry.size + 2047) / 2048;
uint8_t* buf = new uint8_t[totalSectors * 2048];
static constexpr uint32_t kChunkSectors = 32; // 64 KB per chunk
uint32_t sectorsRead = 0;
while (sectorsRead < totalSectors) {
uint32_t toRead = totalSectors - sectorsRead;
if (toRead > kChunkSectors) toRead = kChunkSectors;
bool ok = m_cdrom.readSectorsBlocking(
entry.LBA + sectorsRead, toRead,
buf + sectorsRead * 2048, *m_gpu);
if (!ok) {
delete[] buf;
return nullptr;
}
sectorsRead += toRead;
if (progress && progress->fn) {
uint8_t pct = progress->startPercent +
(uint8_t)((uint32_t)(progress->endPercent - progress->startPercent)
* sectorsRead / totalSectors);
progress->fn(pct, progress->userData);
}
}
outSize = static_cast<int>(entry.size);
return buf;
}
// ── LoadFileSync ─────────────────────────────────────────────
// Blocking fallback for code paths that can't use tasks (e.g.
// SceneManager scene transitions). Uses the blocking readSectors
// variant which spins on GPU callbacks.
uint8_t* LoadFileSync(const char* filename, int& outSize) override {
outSize = 0;
if (!m_isoParser.initialized()) return nullptr;
// --- getDirentry (blocking via one-shot TaskQueue) ---
psyqo::ISO9660Parser::DirEntry entry;
bool found = false;
m_syncQueue
.startWith(m_isoParser.scheduleGetDirentry(filename, &entry))
.then([&found](psyqo::TaskQueue::Task* t) {
found = true;
t->resolve();
})
.butCatch([](psyqo::TaskQueue*) {})
.run();
// Spin until the queue finishes (GPU callbacks service the CD IRQs).
while (m_syncQueue.isRunning()) {
m_gpu->pumpCallbacks();
}
if (!found || entry.type == psyqo::ISO9660Parser::DirEntry::INVALID)
return nullptr;
// --- read sectors (blocking API) ---
uint32_t sectors = (entry.size + 2047) / 2048;
uint8_t* buf = new uint8_t[sectors * 2048];
bool ok = m_cdrom.readSectorsBlocking(
entry.LBA, sectors, buf, *m_gpu);
if (!ok) {
delete[] buf;
return nullptr;
}
outSize = static_cast<int>(entry.size);
return buf;
}
// ── FreeFile ─────────────────────────────────────────────────
void FreeFile(uint8_t* data) override { delete[] data; }
const char* Name() const override { return "cdrom"; }
/** Stash the GPU pointer so LoadFileSync can spin on pumpCallbacks. */
void setGPU(psyqo::GPU* gpu) { m_gpu = gpu; }
private:
psyqo::CDRomDevice m_cdrom;
psyqo::ISO9660Parser m_isoParser;
// Sub-queues (not nested in the parent queue's fixed_vector storage).
psyqo::TaskQueue m_initQueue;
psyqo::TaskQueue m_readQueue;
psyqo::TaskQueue m_syncQueue;
// State carried across the async getDirentry→read chain.
psyqo::ISO9660Parser::ReadRequest m_request;
psyqo::TaskQueue::Task* m_pendingTask = nullptr;
uint8_t** m_pOutBuffer = nullptr;
int* m_pOutSize = nullptr;
psyqo::GPU* m_gpu = nullptr;
};
} // namespace psxsplash

88
src/fileloader_pcdrv.hh Normal file
View File

@@ -0,0 +1,88 @@
#pragma once
#include "fileloader.hh"
#include "pcdrv_handler.hh"
namespace psxsplash {
/**
* FileLoaderPCdrv — loads files via the PCdrv protocol.
*
* Works transparently in two modes (handled by pcdrv_handler.hh):
* - Emulator mode: break instructions intercepted by PCSX-Redux
* - Real hardware mode: SIO1 serial protocol (break handler installed
* by pcdrv_sio1_init, then pcdrv_init detects which path to use)
*
* Both modes are preserved — we call pcdrv_sio1_init() first (installs
* the SIO1 break handler + redirects printf), then pcdrv_init() which
* checks for the PCSX-Redux emulator and falls back to SIO1 if absent.
*
* All tasks resolve synchronously since PCdrv I/O is blocking.
*/
class FileLoaderPCdrv final : public FileLoader {
public:
// ── prepare: no-op for PCdrv ──────────────────────────────────
void prepare() override {}
// ── scheduleInit ──────────────────────────────────────────────
psyqo::TaskQueue::Task scheduleInit() override {
return psyqo::TaskQueue::Task([this](psyqo::TaskQueue::Task* task) {
// Set up SIO1 break handler + printf redirect (real-hardware path).
// On emulator this is harmless — pcsx-redux ignores SIO1 setup.
pcdrv_sio1_init();
// Detect emulator vs SIO1 and initialise the active transport.
m_available = (pcdrv_init() == 0);
task->resolve();
});
}
// ── scheduleLoadFile ──────────────────────────────────────────
psyqo::TaskQueue::Task scheduleLoadFile(
const char* filename, uint8_t*& outBuffer, int& outSize) override
{
return psyqo::TaskQueue::Task(
[this, filename, &outBuffer, &outSize](psyqo::TaskQueue::Task* task) {
outBuffer = doLoad(filename, outSize);
task->complete(outBuffer != nullptr);
});
}
// ── LoadFileSync ──────────────────────────────────────────────
uint8_t* LoadFileSync(const char* filename, int& outSize) override {
return doLoad(filename, outSize);
}
// ── FreeFile ──────────────────────────────────────────────────
void FreeFile(uint8_t* data) override { delete[] data; }
const char* Name() const override { return "pcdrv"; }
private:
bool m_available = false;
uint8_t* doLoad(const char* filename, int& outSize) {
outSize = 0;
if (!m_available) return nullptr;
int fd = pcdrv_open(filename, 0, 0);
if (fd < 0) return nullptr;
int size = pcdrv_seek(fd, 0, 2); // SEEK_END
if (size <= 0) { pcdrv_close(fd); return nullptr; }
pcdrv_seek(fd, 0, 0); // SEEK_SET
// 4-byte aligned for safe struct casting
int aligned = (size + 3) & ~3;
uint8_t* buf = new uint8_t[aligned];
int read = pcdrv_read(fd, buf, size);
pcdrv_close(fd);
if (read != size) { delete[] buf; return nullptr; }
outSize = size;
return buf;
}
};
} // namespace psxsplash

View File

@@ -1,5 +1,5 @@
#include "loadingscreen.hh"
#include "sceneloader.hh"
#include "fileloader.hh"
#include "renderer.hh"
#include <psyqo/primitives/rectangles.hh>
@@ -10,6 +10,12 @@
namespace psxsplash {
// psyqo places the second framebuffer at VRAM Y=256 regardless of the
// actual display height (240 for NTSC, 256 for PAL). All double-buffer
// rendering must use this constant, NOT m_resH, to match the display
// registers set by GPU::flip().
static constexpr int16_t kBuffer1Y = 256;
// ────────────────────────────────────────────────────────────────
// Bare-metal string helpers (no libc)
// ────────────────────────────────────────────────────────────────
@@ -26,32 +32,20 @@ static void int_to_str(int val, char* buf) {
// Load
// ────────────────────────────────────────────────────────────────
bool LoadingScreen::load(psyqo::GPU& gpu, psyqo::Font<>& systemFont, int sceneIndex) {
// Build filename: "scene_N.loading"
char filename[32] = "scene_";
char numBuf[8];
int_to_str(sceneIndex, numBuf);
int i = 6;
for (int j = 0; numBuf[j]; j++) filename[i++] = numBuf[j];
filename[i++] = '.';
filename[i++] = 'l';
filename[i++] = 'o';
filename[i++] = 'a';
filename[i++] = 'd';
filename[i++] = 'i';
filename[i++] = 'n';
filename[i++] = 'g';
filename[i] = '\0';
// Build filename using the active backend's naming convention
char filename[32];
FileLoader::BuildLoadingFilename(sceneIndex, filename, sizeof(filename));
int fileSize = 0;
uint8_t* data = SceneLoader::LoadFile(filename, fileSize);
uint8_t* data = FileLoader::Get().LoadFileSync(filename, fileSize);
if (!data || fileSize < (int)sizeof(LoaderPackHeader)) {
if (data) SceneLoader::FreeFile(data);
if (data) FileLoader::Get().FreeFile(data);
return false;
}
auto* header = reinterpret_cast<const LoaderPackHeader*>(data);
if (header->magic[0] != 'L' || header->magic[1] != 'P') {
SceneLoader::FreeFile(data);
FileLoader::Get().FreeFile(data);
return false;
}
@@ -354,15 +348,15 @@ void LoadingScreen::renderInitialAndFree(psyqo::GPU& gpu) {
renderToBuffer(gpu, 0);
gpu.pumpCallbacks();
// Render to framebuffer 1 (Y = m_resH, typically 240)
renderToBuffer(gpu, m_resH);
// Render to framebuffer 1 (Y = 256 — psyqo's hardcoded buffer-1 offset)
renderToBuffer(gpu, kBuffer1Y);
gpu.pumpCallbacks();
// Restore normal scissor for the active framebuffer
gpu.enableScissor();
// FREE all loaded data — the splashpack needs this memory
SceneLoader::FreeFile(m_data);
FileLoader::Get().FreeFile(m_data);
m_data = nullptr;
m_dataSize = 0;
// m_ui now points into freed memory — don't use it again.
@@ -380,7 +374,7 @@ void LoadingScreen::updateProgress(psyqo::GPU& gpu, uint8_t percent) {
// Draw into both framebuffers using DrawingOffset
for (int buf = 0; buf < 2; buf++) {
int16_t yOff = (buf == 0) ? 0 : m_resH;
int16_t yOff = (buf == 0) ? 0 : kBuffer1Y;
gpu.sendPrimitive(psyqo::Prim::DrawingAreaStart(psyqo::Vertex{{.x = 0, .y = yOff}}));
gpu.sendPrimitive(psyqo::Prim::DrawingAreaEnd(psyqo::Vertex{{.x = m_resW, .y = (int16_t)(yOff + m_resH)}}));

View File

@@ -4,13 +4,16 @@
#include <psyqo/font.hh>
#include <psyqo/gpu.hh>
#include <psyqo/scene.hh>
#include <psyqo/task.hh>
#include <psyqo/trigonometry.hh>
#include "renderer.hh"
#include "scenemanager.hh"
#include "sceneloader.hh"
#include "pcdrv_handler.hh"
#include "loadingscreen.hh"
#include "fileloader.hh"
#if defined(LOADER_CDROM)
#include "fileloader_cdrom.hh"
#endif
namespace {
@@ -30,11 +33,10 @@ class MainScene final : public psyqo::Scene {
psxsplash::SceneManager m_sceneManager;
// Loading screen (persists between scenes; owned here, passed to SceneManager)
psxsplash::LoadingScreen m_loadingScreen;
// PCdrv-loaded scene data (owned)
uint8_t* m_sceneData = nullptr;
// Task queue for async FileLoader init (CD-ROM reset + ISO parse).
// After init completes, loadScene() handles everything synchronously.
psyqo::TaskQueue m_initQueue;
bool m_ready = false;
};
PSXSplash app;
@@ -52,6 +54,16 @@ void PSXSplash::prepare() {
// Initialize the Renderer singleton
psxsplash::Renderer::Init(gpu());
// Let the active file-loader backend do any early setup.
// CDRom: CDRomDevice::prepare() must happen here.
psxsplash::FileLoader::Get().prepare();
#if defined(LOADER_CDROM)
// The CD-ROM backend needs a GPU pointer for LoadFileSync's spin loop.
static_cast<psxsplash::FileLoaderCDRom&>(
psxsplash::FileLoader::Get()).setGPU(&gpu());
#endif
}
void PSXSplash::createScene() {
@@ -61,42 +73,31 @@ void PSXSplash::createScene() {
}
void MainScene::start(StartReason reason) {
// On real hardware: register break handler for PCDRV over SIO1 + redirect printf
// On emulator: no-op (pcsx-redux handles PCDRV natively)
psxsplash::pcdrv_sio1_init();
// Initialise the FileLoader backend, then load scene 0 through
// the same SceneManager::loadScene() path used for all transitions.
//
// For PCdrv the init task resolves synchronously so both steps
// execute in one go. For CD-ROM the init is async (drive reset +
// ISO9660 parse) and yields to the main loop until complete.
// Initialize PCdrv (break instructions - handled by emulator or our break handler)
psxsplash::SceneLoader::Init();
// Blank display immediately so the user never sees a frozen frame
gpu().clear(psyqo::Color{.r = 0, .g = 0, .b = 0});
gpu().pumpCallbacks();
// Try to load a loading screen for scene 0
if (m_loadingScreen.load(gpu(), app.m_font, 0)) {
m_loadingScreen.renderInitialAndFree(gpu());
}
// 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);
if (!m_sceneData) {
// Fallback: try legacy name for backwards compatibility
m_sceneData = psxsplash::SceneLoader::LoadFile("output.bin", fileSize);
}
if (m_loadingScreen.isActive()) {
m_loadingScreen.updateProgress(gpu(), 30);
}
if (m_sceneData) {
m_sceneManager.InitializeScene(m_sceneData, &m_loadingScreen);
}
m_initQueue
.startWith(psxsplash::FileLoader::Get().scheduleInit())
.then([this](psyqo::TaskQueue::Task* task) {
m_sceneManager.loadScene(gpu(), 0, /*isFirstScene=*/true);
m_ready = true;
task->resolve();
})
.butCatch([](psyqo::TaskQueue*) {
// FileLoader init failed — nothing we can do on PS1.
})
.run();
}
void MainScene::frame() {
// Don't run the game loop while FileLoader init is still executing
// (only relevant for the async CD-ROM backend).
if (!m_ready) return;
uint32_t beginFrame = gpu().now();
auto currentFrameCounter = gpu().getFrameCount();
auto deltaTime = currentFrameCounter - mainScene.m_lastFrameCounter;

View File

@@ -106,6 +106,11 @@ public:
/// Is nav data loaded?
bool isLoaded() const { return m_regions != nullptr; }
void relocate(intptr_t delta) {
if (m_regions) m_regions = reinterpret_cast<const NavRegion*>(reinterpret_cast<intptr_t>(m_regions) + delta);
if (m_portals) m_portals = reinterpret_cast<const NavPortal*>(reinterpret_cast<intptr_t>(m_portals) + delta);
}
/// Get the number of regions
uint16_t getRegionCount() const { return m_header.regionCount; }

View File

@@ -1,65 +1,29 @@
#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"
#include "fileloader.hh"
namespace psxsplash {
bool SceneLoader::s_pcdrvAvailable = false;
bool SceneLoader::s_initialised = false;
bool SceneLoader::Init() {
s_pcdrvAvailable = (pcdrv_init() == 0);
return s_pcdrvAvailable;
// FileLoader::Get().scheduleInit() is task-based; for backward compat
// with the old sync Init() we rely on main.cpp running the init task
// before any LoadFile calls. This flag is set unconditionally so that
// IsPCdrvAvailable() remains useful.
s_initialised = true;
return true;
}
bool SceneLoader::IsPCdrvAvailable() {
return s_pcdrvAvailable;
return s_initialised;
}
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;
return FileLoader::Get().LoadFileSync(filename, outSize);
}
void SceneLoader::FreeFile(uint8_t* data) {
delete[] data;
FileLoader::Get().FreeFile(data);
}
} // namespace psxsplash

View File

@@ -5,45 +5,27 @@
namespace psxsplash {
/**
* SceneLoader — loads splashpack files from PCdrv (emulator) or CD-ROM.
* SceneLoader — backward-compatibility façade.
*
* 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.
* All calls now delegate to the FileLoader singleton (selected at compile
* time by the LOADER build flag). New code should use FileLoader directly.
*/
class SceneLoader {
public:
/**
* Initialize the loader. Must be called once at startup.
* Returns true if PCdrv is available, false otherwise.
*/
/** Initialise the active FileLoader backend (PCdrv or CD-ROM). */
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.
*/
/** Load a file synchronously. Uses FileLoader::Get().LoadFileSync(). */
static uint8_t* LoadFile(const char* filename, int& outSize);
/**
* Free previously loaded file data.
*/
/** Free previously loaded data. Uses FileLoader::Get().FreeFile(). */
static void FreeFile(uint8_t* data);
/**
* Returns true if PCdrv is available.
*/
/** Returns true after a successful Init(). */
static bool IsPCdrvAvailable();
private:
static bool s_pcdrvAvailable;
static bool s_initialised;
};
} // namespace psxsplash

View File

@@ -9,6 +9,8 @@
#include "luaapi.hh"
#include "loadingscreen.hh"
#include <psyqo/primitives/misc.hh>
#include "lua.h"
using namespace psyqo::trig_literals;
@@ -221,6 +223,11 @@ void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingSc
L.OnSceneCreationEnd();
if (loading && loading->isActive()) loading->updateProgress(gpu, 95);
m_liveDataSize = sceneSetup.liveDataSize;
shrinkBuffer();
if (loading && loading->isActive()) loading->updateProgress(gpu, 100);
}
@@ -605,7 +612,7 @@ void psxsplash::SceneManager::processEnableDisableEvents() {
}
// ============================================================================
// SCENE LOADING (PCdrv multi-scene)
// SCENE LOADING
// ============================================================================
void psxsplash::SceneManager::requestSceneLoad(int sceneIndex) {
@@ -620,42 +627,74 @@ void psxsplash::SceneManager::processPendingSceneLoad() {
m_pendingSceneIndex = -1;
auto& gpu = Renderer::GetInstance().getGPU();
loadScene(gpu, targetIndex, /*isFirstScene=*/false);
}
// Build filename: scene_N.splashpack
void psxsplash::SceneManager::loadScene(psyqo::GPU& gpu, int sceneIndex, bool isFirstScene) {
// Build filename using the active backend's naming convention
char filename[32];
snprintf(filename, sizeof(filename), "scene_%d.splashpack", targetIndex);
FileLoader::BuildSceneFilename(sceneIndex, filename, sizeof(filename));
// Blank the screen immediately so the user doesn't see a frozen frame
gpu.clear(psyqo::Color{.r = 0, .g = 0, .b = 0});
// Blank BOTH framebuffers so the user doesn't see the BIOS screen
// or a frozen frame. FastFill ignores the scissor/DrawingArea so we
// can target any VRAM region directly.
psyqo::Prim::FastFill ff(psyqo::Color{.r = 0, .g = 0, .b = 0});
ff.rect = psyqo::Rect{0, 0, 320, 240};
gpu.sendPrimitive(ff);
ff.rect = psyqo::Rect{0, 256, 320, 240};
gpu.sendPrimitive(ff);
gpu.pumpCallbacks();
// Try to load a loading screen for the target scene
LoadingScreen loading;
if (s_font) {
if (loading.load(gpu, *s_font, targetIndex)) {
if (loading.load(gpu, *s_font, sceneIndex)) {
loading.renderInitialAndFree(gpu);
}
}
// 1. Tear down EVERYTHING in the current scene first —
if (!isFirstScene) {
// Tear down EVERYTHING in the current scene first —
// Lua VM, vector backing storage, audio. This returns as much
// heap memory as possible before any new allocation.
clearScene();
// 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.
// Free old splashpack data BEFORE loading the new one.
// This avoids having both scene buffers in the heap simultaneously.
if (m_currentSceneData) {
SceneLoader::FreeFile(m_currentSceneData);
FileLoader::Get().FreeFile(m_currentSceneData);
m_currentSceneData = nullptr;
}
}
if (loading.isActive()) loading.updateProgress(gpu, 20);
// 3. Allocate new scene data (heap is now maximally consolidated)
// Load scene data — use progress-aware variant so the loading bar
// animates during the (potentially slow) CD-ROM read.
int fileSize = 0;
uint8_t* newData = SceneLoader::LoadFile(filename, fileSize);
uint8_t* newData = nullptr;
if (loading.isActive()) {
struct Ctx { LoadingScreen* ls; psyqo::GPU* gpu; };
Ctx ctx{&loading, &gpu};
FileLoader::LoadProgressInfo progress{
[](uint8_t pct, void* ud) {
auto* c = static_cast<Ctx*>(ud);
c->ls->updateProgress(*c->gpu, pct);
},
&ctx, 20, 30
};
newData = FileLoader::Get().LoadFileSyncWithProgress(
filename, fileSize, &progress);
} else {
newData = FileLoader::Get().LoadFileSync(filename, fileSize);
}
if (!newData && isFirstScene) {
// Fallback: try legacy name for backwards compatibility (PCdrv only)
newData = FileLoader::Get().LoadFileSync("output.bin", fileSize);
}
if (!newData) {
return;
}
@@ -663,9 +702,9 @@ void psxsplash::SceneManager::processPendingSceneLoad() {
if (loading.isActive()) loading.updateProgress(gpu, 30);
m_currentSceneData = newData;
m_currentSceneIndex = targetIndex;
m_currentSceneIndex = sceneIndex;
// 4. Initialize with new data (creates fresh Lua VM inside)
// Initialize with new data (creates fresh Lua VM inside)
InitializeScene(newData, loading.isActive() ? &loading : nullptr);
}
@@ -703,6 +742,58 @@ void psxsplash::SceneManager::clearScene() {
m_sceneType = 0;
}
void psxsplash::SceneManager::shrinkBuffer() {
if (m_liveDataSize == 0 || m_currentSceneData == nullptr) return;
uint8_t* oldBase = m_currentSceneData;
uint8_t* newBase = new uint8_t[m_liveDataSize];
if (!newBase) return;
__builtin_memcpy(newBase, oldBase, m_liveDataSize);
intptr_t delta = reinterpret_cast<intptr_t>(newBase) - reinterpret_cast<intptr_t>(oldBase);
auto reloc = [delta](auto* ptr) -> decltype(ptr) {
if (!ptr) return ptr;
return reinterpret_cast<decltype(ptr)>(reinterpret_cast<intptr_t>(ptr) + delta);
};
for (auto& go : m_gameObjects) {
go = reloc(go);
go->polygons = reloc(go->polygons);
}
for (auto& lf : m_luaFiles) {
lf = reloc(lf);
lf->luaCode = reloc(lf->luaCode);
}
for (auto& name : m_objectNames) name = reloc(name);
for (auto& name : m_audioClipNames) name = reloc(name);
for (auto& inter : m_interactables) inter = reloc(inter);
m_bvh.relocate(delta);
m_worldCollision.relocate(delta);
m_navRegions.relocate(delta);
m_rooms = reloc(m_rooms);
m_portals = reloc(m_portals);
m_roomTriRefs = reloc(m_roomTriRefs);
for (int ci = 0; ci < m_cutsceneCount; ci++) {
auto& cs = m_cutscenes[ci];
cs.name = reloc(cs.name);
cs.audioEvents = reloc(cs.audioEvents);
for (uint8_t ti = 0; ti < cs.trackCount; ti++) {
auto& track = cs.tracks[ti];
track.keyframes = reloc(track.keyframes);
if (track.target) track.target = reloc(track.target);
}
}
m_uiSystem.relocate(delta);
FileLoader::Get().FreeFile(oldBase);
m_currentSceneData = newBase;
}
// ============================================================================
// OBJECT NAME LOOKUP
// ============================================================================

View File

@@ -18,7 +18,7 @@
#include "audiomanager.hh"
#include "interactable.hh"
#include "luaapi.hh"
#include "sceneloader.hh"
#include "fileloader.hh"
#include "cutscene.hh"
#include "uisystem.hh"
@@ -81,10 +81,19 @@ class SceneManager {
Lua& getLua() { return L; }
AudioManager& getAudio() { return m_audio; }
// Scene loading (for multi-scene support via PCdrv)
// Scene loading (for multi-scene support)
void requestSceneLoad(int sceneIndex);
int getCurrentSceneIndex() const { return m_currentSceneIndex; }
/// Load a scene by index. This is the ONE canonical load path used by
/// both the initial boot (main.cpp) and runtime scene transitions.
/// Blanks the screen, shows a loading screen, tears down the old scene,
/// loads the new splashpack, and initialises.
/// @param gpu GPU reference.
/// @param sceneIndex Scene to load.
/// @param isFirstScene True when called from boot (skips clearScene / free).
void loadScene(psyqo::GPU& gpu, int sceneIndex, bool isFirstScene = false);
// Check and process pending scene load (called from GameTick)
void processPendingSceneLoad();
@@ -160,11 +169,13 @@ class SceneManager {
int m_currentSceneIndex = 0;
int m_pendingSceneIndex = -1; // -1 = no pending load
uint8_t* m_currentSceneData = nullptr; // Owned pointer to loaded data
uint32_t m_liveDataSize = 0; // Bytes of m_currentSceneData still needed at runtime
// System update methods (called from GameTick)
void updateInteractionSystem();
void processEnableDisableEvents();
void clearScene(); // Deallocate current scene objects
void shrinkBuffer(); // Free pixel/audio bulk data after VRAM/SPU uploads
};
}; // namespace psxsplash
// namespace psxsplash

View File

@@ -27,69 +27,53 @@ static bool sp_streq(const char* a, const char* b) {
}
struct SPLASHPACKFileHeader {
char magic[2]; // "SP"
uint16_t version; // Format version (8 = movement params)
char magic[2];
uint16_t version;
uint16_t luaFileCount;
uint16_t gameObjectCount;
uint16_t navmeshCount;
uint16_t textureAtlasCount;
uint16_t clutCount;
uint16_t colliderCount;
uint16_t interactableCount;
psyqo::GTE::PackedVec3 playerStartPos;
psyqo::GTE::PackedVec3 playerStartRot;
psyqo::FixedPoint<12, uint16_t> playerHeight;
uint16_t 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 sceneType;
uint16_t pad0;
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 moveSpeed;
uint16_t sprintSpeed;
uint16_t jumpVelocity;
uint16_t gravity;
uint16_t playerRadius;
uint16_t pad1;
uint32_t nameTableOffset;
uint16_t audioClipCount;
uint16_t pad2;
uint32_t audioTableOffset;
uint8_t fogEnabled;
uint8_t fogR, fogG, fogB;
uint8_t fogDensity;
uint8_t pad3;
uint16_t roomCount;
uint16_t portalCount;
uint16_t roomTriRefCount;
// Version 12 additions (cutscenes):
uint16_t cutsceneCount;
uint16_t reserved_cs;
uint16_t pad4;
uint32_t cutsceneTableOffset;
// Version 13 additions (UI system + fonts):
uint16_t uiCanvasCount;
uint8_t uiFontCount;
uint8_t uiPad13;
uint8_t uiPad5;
uint32_t uiTableOffset;
uint32_t pixelDataOffset;
};
static_assert(sizeof(SPLASHPACKFileHeader) == 112, "SPLASHPACKFileHeader must be 112 bytes");
static_assert(sizeof(SPLASHPACKFileHeader) == 104, "SPLASHPACKFileHeader must be 104 bytes");
struct SPLASHPACKTextureAtlas {
uint32_t polygonsOffset;
@@ -109,7 +93,7 @@ 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 >= 13, "Splashpack version too old (need v13+): re-export from SplashEdit");
psyqo::Kernel::assert(header->version >= 15, "Splashpack version too old (need v15+): re-export from SplashEdit");
setup.playerStartPosition = header->playerStartPos;
setup.playerStartRotation = header->playerStartRot;
@@ -125,13 +109,8 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
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++) {
@@ -158,8 +137,8 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += sizeof(psxsplash::SPLASHPACKCollider);
}
// Read BVH data (version 3+)
if (header->version >= 3 && header->bvhNodeCount > 0) {
// BVH data
if (header->bvhNodeCount > 0) {
BVHNode* bvhNodes = reinterpret_cast<BVHNode*>(cursor);
cursor += header->bvhNodeCount * sizeof(BVHNode);
@@ -170,8 +149,6 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
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);
@@ -179,48 +156,22 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
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) {
// World collision soup
if (header->worldCollisionMeshCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.worldCollision.initializeFromData(cursor));
}
// Read nav regions (version 7+)
if (header->version >= 7 && header->navRegionCount > 0) {
// Nav regions
if (header->navRegionCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
cursor = const_cast<uint8_t*>(setup.navRegions.initializeFromData(cursor));
}
// 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) {
// Room/portal data (interior scenes)
if (header->roomCount > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(cursor);
cursor = reinterpret_cast<uint8_t*>((addr + 3) & ~3);
@@ -237,9 +188,6 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
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;
@@ -256,8 +204,7 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
cursor += sizeof(psxsplash::SPLASHPACKClut);
}
// Read object name table (version 9+)
if (header->version >= 9 && header->nameTableOffset != 0) {
if (header->nameTableOffset != 0) {
uint8_t* nameData = data + header->nameTableOffset;
setup.objectNames.reserve(header->gameObjectCount);
for (uint16_t i = 0; i < header->gameObjectCount; i++) {
@@ -269,10 +216,7 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
}
}
// 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
if (header->audioClipCount > 0 && header->audioTableOffset != 0) {
uint8_t* audioTable = data + header->audioTableOffset;
setup.audioClips.reserve(header->audioClipCount);
setup.audioClipNames.reserve(header->audioClipCount);
@@ -294,20 +238,14 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
}
}
// 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;
// Read cutscene data (version 12+)
if (header->version >= 12 && header->cutsceneCount > 0 && header->cutsceneTableOffset != 0) {
if (header->cutsceneCount > 0 && header->cutsceneTableOffset != 0) {
setup.cutsceneCount = 0;
uint8_t* tablePtr = data + header->cutsceneTableOffset;
int csCount = header->cutsceneCount;
@@ -404,6 +342,8 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup
setup.uiTableOffset = header->uiTableOffset;
}
setup.liveDataSize = header->pixelDataOffset;
}
} // namespace psxsplash

View File

@@ -96,6 +96,11 @@ struct SplashpackSceneSetup {
uint16_t uiCanvasCount = 0;
uint8_t uiFontCount = 0;
uint32_t uiTableOffset = 0;
// Buffer management (v15+): byte offset where pixel/audio bulk data begins.
// Everything before this offset is needed at runtime; everything after can
// be freed once VRAM/SPU uploads are done.
uint32_t liveDataSize = 0;
};
class SplashPackLoader {

View File

@@ -29,6 +29,24 @@ void UISystem::init(psyqo::Font<>& systemFont) {
m_fontCount = 0;
}
// ============================================================================
// Pointer relocation after buffer shrink
// ============================================================================
void UISystem::relocate(intptr_t delta) {
for (int ci = 0; ci < m_canvasCount; ci++) {
if (m_canvases[ci].name && m_canvases[ci].name[0] != '\0')
m_canvases[ci].name = reinterpret_cast<const char*>(reinterpret_cast<intptr_t>(m_canvases[ci].name) + delta);
}
for (int ei = 0; ei < m_elementCount; ei++) {
if (m_elements[ei].name && m_elements[ei].name[0] != '\0')
m_elements[ei].name = reinterpret_cast<const char*>(reinterpret_cast<intptr_t>(m_elements[ei].name) + delta);
}
for (int fi = 0; fi < m_fontCount; fi++) {
m_fontDescs[fi].pixelData = nullptr;
}
}
// ============================================================================
// Load from splashpack (zero-copy, pointer fixup)
// ============================================================================
@@ -61,24 +79,8 @@ void UISystem::loadFromSplashpack(uint8_t* data, uint16_t canvasCount,
ptr += 112;
}
// ── Skip past font pixel data to reach canvas descriptors ──
// The binary layout is: [font descriptors] [font pixel data] [canvas descriptors]
// ptr is currently past the font descriptors. We need to skip the pixel data block.
// Pixel data positions are stored as absolute offsets in the descriptors.
if (fontCount > 0) {
uint32_t fontDataEnd = 0;
for (int fi = 0; fi < fontCount; fi++) {
if (m_fontDescs[fi].pixelData != nullptr && m_fontDescs[fi].pixelDataSize > 0) {
uint32_t endPos = (uint32_t)(m_fontDescs[fi].pixelData - data) + m_fontDescs[fi].pixelDataSize;
if (endPos > fontDataEnd) fontDataEnd = endPos;
}
}
if (fontDataEnd > 0) {
// Align to 4 bytes (matching the writer's AlignToFourBytes)
fontDataEnd = (fontDataEnd + 3) & ~3u;
ptr = data + fontDataEnd;
}
}
// Canvas descriptors follow immediately after font descriptors.
// Font pixel data is in the dead zone (at absolute offsets in the descriptors).
// ── Parse canvas descriptors ──
if (canvasCount == 0) return;

View File

@@ -82,6 +82,8 @@ public:
/// Must be called AFTER loadFromSplashpack and BEFORE first render.
void uploadFonts(psyqo::GPU& gpu);
void relocate(intptr_t delta);
// Phase 1: Insert OT primitives for boxes, images, progress bars, and custom font text.
// Called BEFORE gpu.chain(ot) from inside the renderer.
void renderOT(psyqo::GPU& gpu,

View File

@@ -142,6 +142,12 @@ public:
/// Is collision data loaded?
bool isLoaded() const { return m_triangles != nullptr; }
void relocate(intptr_t delta) {
if (m_meshes) m_meshes = reinterpret_cast<const CollisionMeshHeader*>(reinterpret_cast<intptr_t>(m_meshes) + delta);
if (m_triangles) m_triangles = reinterpret_cast<const CollisionTri*>(reinterpret_cast<intptr_t>(m_triangles) + delta);
if (m_chunks) m_chunks = reinterpret_cast<const CollisionChunk*>(reinterpret_cast<intptr_t>(m_chunks) + delta);
}
// ========================================================================
// High-level queries used by the player movement system
// ========================================================================