Broken runtime
This commit is contained in:
7
Makefile
7
Makefile
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
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;
|
||||
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,17 +91,15 @@ 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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,7 +38,7 @@ 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.
|
||||
/// 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);
|
||||
|
||||
@@ -67,7 +46,7 @@ public:
|
||||
/// 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
|
||||
|
||||
121
src/bvh.cpp
121
src/bvh.cpp
@@ -3,63 +3,42 @@
|
||||
namespace psxsplash {
|
||||
|
||||
void BVHManager::initialize(const BVHNode *nodes, uint16_t nodeCount,
|
||||
const TriangleRef* triangleRefs, uint16_t triangleRefCount) {
|
||||
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];
|
||||
|
||||
// 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];
|
||||
|
||||
// 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
|
||||
|
||||
41
src/bvh.hh
41
src/bvh.hh
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
116
src/collision.hh
116
src/collision.hh
@@ -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
46
src/fileloader.cpp
Normal 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
116
src/fileloader.hh
Normal 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
234
src/fileloader_cdrom.hh
Normal 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
88
src/fileloader_pcdrv.hh
Normal 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
|
||||
@@ -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)}}));
|
||||
|
||||
81
src/main.cpp
81
src/main.cpp
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
// ============================================================================
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user