diff --git a/Makefile b/Makefile index 5b835ce..1899fbd 100644 --- a/Makefile +++ b/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 diff --git a/src/audiomanager.cpp b/src/audiomanager.cpp index 2689cf0..6c7492d 100644 --- a/src/audiomanager.cpp +++ b/src/audiomanager.cpp @@ -8,227 +8,176 @@ namespace psxsplash { -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - uint16_t AudioManager::volToHw(int v) { - if (v <= 0) return 0; - if (v >= 128) return 0x3fff; - return static_cast((v * 0x3fff) / 128); + if (v <= 0) + return 0; + if (v >= 128) + return 0x3fff; + return static_cast((v * 0x3fff) / 128); } -// --------------------------------------------------------------------------- -// Init / Reset -// --------------------------------------------------------------------------- - void AudioManager::init() { - psyqo::SPU::initialize(); + psyqo::SPU::initialize(); - m_nextAddr = SPU_RAM_START; + m_nextAddr = SPU_RAM_START; - for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { - m_clips[i].loaded = false; - } + for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { + m_clips[i].loaded = false; + } } void AudioManager::reset() { - stopAll(); - for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { - m_clips[i].loaded = false; - } - m_nextAddr = SPU_RAM_START; + stopAll(); + for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { + m_clips[i].loaded = false; + } + m_nextAddr = SPU_RAM_START; } -// --------------------------------------------------------------------------- -// Clip loading -// --------------------------------------------------------------------------- +bool AudioManager::loadClip(int clipIndex, const uint8_t *adpcmData, + uint32_t sizeBytes, uint16_t sampleRate, + bool loop) { + if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS) + return false; + if (!adpcmData || sizeBytes == 0) + return false; -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 and skip VAG header if present + if (sizeBytes >= 48) { + const char *magic = reinterpret_cast(adpcmData); + if (magic[0] == 'V' && magic[1] == 'A' && magic[2] == 'G' && + magic[3] == 'p') { + adpcmData += 48; + sizeBytes -= 48; + } + } - // Check for VAG header (magic "VAGp" at offset 0). - // If present, the header wasn't stripped properly — skip it. - if (sizeBytes >= 48) { - const char* magic = reinterpret_cast(adpcmData); - if (magic[0] == 'V' && magic[1] == 'A' && magic[2] == 'G' && magic[3] == 'p') { - adpcmData += 48; - sizeBytes -= 48; - } + uint32_t addr = (m_nextAddr + 15) & ~15u; + uint32_t alignedSize = (sizeBytes + 15) & ~15u; + + if (addr + alignedSize > SPU_RAM_END) { + return false; + } + + const uint8_t *src = adpcmData; + uint32_t remaining = alignedSize; + uint32_t dstAddr = addr; + while (remaining > 0) { + uint32_t bytesThisRound = (remaining > 65520u) ? 65520u : remaining; + bytesThisRound &= ~15u; // 16-byte block alignment + if (bytesThisRound == 0) + break; + + uint16_t dmaSizeParam = (uint16_t)(bytesThisRound / 4); + psyqo::SPU::dmaWrite(dstAddr, src, dmaSizeParam, 4); + + while (DMA_CTRL[DMA_SPU].CHCR & (1 << 24)) { } - // Align to 16-byte SPU ADPCM block boundary - uint32_t addr = (m_nextAddr + 15) & ~15u; - uint32_t alignedSize = (sizeBytes + 15) & ~15u; + src += bytesThisRound; + dstAddr += bytesThisRound; + remaining -= bytesThisRound; + } - if (addr + alignedSize > SPU_RAM_END) { - return false; - } + SPU_CTRL &= ~(0b11 << 4); - // 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; + m_clips[clipIndex].spuAddr = addr; + m_clips[clipIndex].size = sizeBytes; + m_clips[clipIndex].sampleRate = sampleRate; + m_clips[clipIndex].loop = loop; + m_clips[clipIndex].loaded = true; - 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)) {} - - 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; - m_clips[clipIndex].size = sizeBytes; - m_clips[clipIndex].sampleRate = sampleRate; - m_clips[clipIndex].loop = loop; - m_clips[clipIndex].loaded = true; - - m_nextAddr = addr + alignedSize; - return true; + m_nextAddr = addr + alignedSize; + return true; } -// --------------------------------------------------------------------------- -// Playback -// --------------------------------------------------------------------------- - int AudioManager::play(int clipIndex, int volume, int pan) { - if (clipIndex < 0 || clipIndex >= MAX_AUDIO_CLIPS || !m_clips[clipIndex].loaded) { - return -1; - } + 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; + uint32_t ch = psyqo::SPU::getNextFreeChannel(); + if (ch == psyqo::SPU::NO_FREE_CHANNEL) + return -1; - const AudioClip& clip = m_clips[clipIndex]; + const AudioClip &clip = m_clips[clipIndex]; - uint16_t vol = volToHw(volume); - uint16_t leftVol = vol; - uint16_t rightVol = vol; - if (pan != 64) { - int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan); - leftVol = (uint16_t)((uint32_t)vol * (127 - p) / 127); - rightVol = (uint16_t)((uint32_t)vol * p / 127); - } + uint16_t vol = volToHw(volume); + uint16_t leftVol = vol; + uint16_t rightVol = vol; + if (pan != 64) { + int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan); + leftVol = (uint16_t)((uint32_t)vol * (127 - p) / 127); + rightVol = (uint16_t)((uint32_t)vol * p / 127); + } - // 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(clip.spuAddr / 8); - } else { - SPU_VOICES[ch].sampleRepeatAddr = DUMMY_SPU_ADDR / 8; - } + constexpr uint16_t DUMMY_SPU_ADDR = 0x1000; + if (clip.loop) { + SPU_VOICES[ch].sampleRepeatAddr = static_cast(clip.spuAddr / 8); + } else { + SPU_VOICES[ch].sampleRepeatAddr = DUMMY_SPU_ADDR / 8; + } - // Build playback config - psyqo::SPU::ChannelPlaybackConfig config; - config.sampleRate.value = static_cast(((uint32_t)clip.sampleRate << 12) / 44100); - config.volumeLeft = leftVol; - config.volumeRight = rightVol; - config.adsr = DEFAULT_ADSR; + psyqo::SPU::ChannelPlaybackConfig config; + config.sampleRate.value = + static_cast(((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. + if (ch > 15) { + SPU_KEY_OFF_HIGH = 1 << (ch - 16); + } else { + SPU_KEY_OFF_LOW = 1 << ch; + } - // KEY_OFF (hard cut) - if (ch > 15) { - SPU_KEY_OFF_HIGH = 1 << (ch - 16); - } else { - SPU_KEY_OFF_LOW = 1 << ch; - } + SPU_VOICES[ch].volumeLeft = config.volumeLeft; + SPU_VOICES[ch].volumeRight = config.volumeRight; + SPU_VOICES[ch].sampleRate = config.sampleRate.value; + SPU_VOICES[ch].sampleStartAddr = static_cast(clip.spuAddr / 8); + SPU_VOICES[ch].ad = config.adsr & 0xFFFF; + SPU_VOICES[ch].sr = (config.adsr >> 16) & 0xFFFF; - SPU_VOICES[ch].volumeLeft = config.volumeLeft; - SPU_VOICES[ch].volumeRight = config.volumeRight; - SPU_VOICES[ch].sampleRate = config.sampleRate.value; - SPU_VOICES[ch].sampleStartAddr = static_cast(clip.spuAddr / 8); - SPU_VOICES[ch].ad = config.adsr & 0xFFFF; - SPU_VOICES[ch].sr = (config.adsr >> 16) & 0xFFFF; + if (ch > 15) { + SPU_KEY_ON_HIGH = 1 << (ch - 16); + } else { + SPU_KEY_ON_LOW = 1 << ch; + } - // KEY_ON - if (ch > 15) { - SPU_KEY_ON_HIGH = 1 << (ch - 16); - } else { - SPU_KEY_ON_LOW = 1 << ch; - } - - return static_cast(ch); + return static_cast(ch); } -// --------------------------------------------------------------------------- -// Stop -// --------------------------------------------------------------------------- - void AudioManager::stopVoice(int channel) { - if (channel < 0 || channel >= MAX_VOICES) return; - psyqo::SPU::silenceChannels(1u << channel); + 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; - uint16_t vol = volToHw(volume); - if (pan == 64) { - SPU_VOICES[channel].volumeLeft = vol; - SPU_VOICES[channel].volumeRight = vol; - } else { - int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan); - SPU_VOICES[channel].volumeLeft = (uint16_t)((uint32_t)vol * (127 - p) / 127); - SPU_VOICES[channel].volumeRight = (uint16_t)((uint32_t)vol * p / 127); - } + if (channel < 0 || channel >= MAX_VOICES) + return; + uint16_t vol = volToHw(volume); + if (pan == 64) { + SPU_VOICES[channel].volumeLeft = vol; + SPU_VOICES[channel].volumeRight = vol; + } else { + int p = pan < 0 ? 0 : (pan > 127 ? 127 : pan); + SPU_VOICES[channel].volumeLeft = + (uint16_t)((uint32_t)vol * (127 - p) / 127); + SPU_VOICES[channel].volumeRight = (uint16_t)((uint32_t)vol * p / 127); + } } -// --------------------------------------------------------------------------- -// Query -// --------------------------------------------------------------------------- - int AudioManager::getLoadedClipCount() const { - int count = 0; - for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { - if (m_clips[i].loaded) count++; - } - return count; + int count = 0; + for (int i = 0; i < MAX_AUDIO_CLIPS; i++) { + if (m_clips[i].loaded) + count++; + } + return count; } -} // namespace psxsplash +} // namespace psxsplash diff --git a/src/audiomanager.hh b/src/audiomanager.hh index 5ca8c14..73118db 100644 --- a/src/audiomanager.hh +++ b/src/audiomanager.hh @@ -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 @@ -55,45 +34,45 @@ struct AudioClip { /// Volume is 0-128 (0=silent, 128=max). Pan is 0-127 (64=center). class AudioManager { public: - /// Initialize SPU hardware and reset state - void init(); + /// Initialize SPU hardware and reset state + void init(); - /// Upload ADPCM data to SPU RAM and register as clip index. - /// Data must be 16-byte aligned (SPU ADPCM block size). Returns true on success. - bool loadClip(int clipIndex, const uint8_t* adpcmData, uint32_t sizeBytes, - uint16_t sampleRate, bool loop); + /// Upload ADPCM data to SPU RAM and register as clip index. + /// Data must be 16-byte aligned. Returns true on success. + bool loadClip(int clipIndex, const uint8_t *adpcmData, uint32_t sizeBytes, + uint16_t sampleRate, bool loop); - /// Play a clip by index. Returns channel (0-23), or -1 if full. - /// Volume: 0-128 (128=max). Pan: 0 (left) to 127 (right), 64 = center. - int play(int clipIndex, int volume = 128, int pan = 64); + /// Play a clip by index. Returns channel (0-23), or -1 if full. + /// Volume: 0-128 (128=max). Pan: 0 (left) to 127 (right), 64 = center. + int play(int clipIndex, int volume = 128, int pan = 64); - /// Stop a specific channel (returned from play()) - void stopVoice(int channel); + /// Stop a specific channel + void stopVoice(int channel); - /// Stop all playing channels - void stopAll(); + /// Stop all playing channels + void stopAll(); - /// Set volume/pan on a playing channel - void setVoiceVolume(int channel, int volume, int pan = 64); + /// Set volume/pan on a playing channel + void setVoiceVolume(int channel, int volume, int pan = 64); - /// Get total SPU RAM used by loaded clips (for visualization) - uint32_t getUsedSPURam() const { return m_nextAddr - SPU_RAM_START; } + /// Get total SPU RAM used by loaded clips + uint32_t getUsedSPURam() const { return m_nextAddr - SPU_RAM_START; } - /// Get total SPU RAM available - uint32_t getTotalSPURam() const { return SPU_RAM_END - SPU_RAM_START; } + /// Get total SPU RAM available + uint32_t getTotalSPURam() const { return SPU_RAM_END - SPU_RAM_START; } - /// Get number of loaded clips - int getLoadedClipCount() const; + /// Get number of loaded clips + int getLoadedClipCount() const; - /// Reset all clips and free SPU RAM (call on scene unload) - void reset(); + /// Reset all clips and free SPU RAM + void reset(); private: - /// Convert 0-128 volume to hardware 0-0x3FFF (fixed-volume mode) - static uint16_t volToHw(int v); + /// Convert 0-128 volume to hardware 0-0x3FFF (fixed-volume mode) + static uint16_t volToHw(int v); - AudioClip m_clips[MAX_AUDIO_CLIPS]; - uint32_t m_nextAddr = SPU_RAM_START; // Bump allocator for SPU RAM + AudioClip m_clips[MAX_AUDIO_CLIPS]; + uint32_t m_nextAddr = SPU_RAM_START; }; -} // namespace psxsplash +} // namespace psxsplash diff --git a/src/bvh.cpp b/src/bvh.cpp index a5bb085..c087077 100644 --- a/src/bvh.cpp +++ b/src/bvh.cpp @@ -2,142 +2,123 @@ namespace psxsplash { -void BVHManager::initialize(const BVHNode* nodes, uint16_t nodeCount, - const TriangleRef* triangleRefs, uint16_t triangleRefCount) { - m_nodes = nodes; - m_nodeCount = nodeCount; - m_triangleRefs = triangleRefs; - m_triangleRefCount = triangleRefCount; +void BVHManager::initialize(const BVHNode *nodes, uint16_t nodeCount, + const TriangleRef *triangleRefs, + uint16_t triangleRefCount) { + m_nodes = nodes; + m_nodeCount = nodeCount; + m_triangleRefs = triangleRefs; + m_triangleRefCount = triangleRefCount; } -const uint8_t* BVHManager::initializeFromData(const uint8_t* data, uint16_t nodeCount, uint16_t triangleRefCount) { - if (data == nullptr || nodeCount == 0) { - m_nodes = nullptr; - m_triangleRefs = nullptr; - m_nodeCount = 0; - m_triangleRefCount = 0; - return data; - } - - // Point to node array - m_nodes = reinterpret_cast(data); - m_nodeCount = nodeCount; - data += m_nodeCount * sizeof(BVHNode); - - // Point to triangle ref array - m_triangleRefs = reinterpret_cast(data); - m_triangleRefCount = triangleRefCount; - data += m_triangleRefCount * sizeof(TriangleRef); - - return data; + +int BVHManager::cullFrustum(const Frustum &frustum, TriangleRef *outRefs, + int maxRefs) const { + if (!isLoaded() || m_nodeCount == 0) + return 0; + + return traverseFrustum(0, frustum, outRefs, 0, maxRefs); } -int BVHManager::cullFrustum(const Frustum& frustum, - TriangleRef* outRefs, - int maxRefs) const { - if (!isLoaded() || m_nodeCount == 0) return 0; - - return traverseFrustum(0, frustum, outRefs, 0, maxRefs); -} - -int BVHManager::traverseFrustum(int nodeIndex, - const Frustum& frustum, - TriangleRef* outRefs, - int currentCount, - int maxRefs) const { - if (nodeIndex < 0 || nodeIndex >= m_nodeCount) return currentCount; - if (currentCount >= maxRefs) return currentCount; - - const BVHNode& node = m_nodes[nodeIndex]; - - // Frustum test - if node is completely outside, skip entire subtree - if (!frustum.testAABB(node)) { - return currentCount; // Culled! - } - - // If leaf, add all triangles - if (node.isLeaf()) { - int count = node.triangleCount; - int available = maxRefs - currentCount; - if (count > available) count = available; - - for (int i = 0; i < count; i++) { - outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i]; - } - return currentCount + count; - } - - // Recurse into children - if (node.leftChild != 0xFFFF) { - currentCount = traverseFrustum(node.leftChild, frustum, outRefs, currentCount, maxRefs); - } - if (node.rightChild != 0xFFFF) { - currentCount = traverseFrustum(node.rightChild, frustum, outRefs, currentCount, maxRefs); - } - +int BVHManager::traverseFrustum(int nodeIndex, const Frustum &frustum, + TriangleRef *outRefs, int currentCount, + int maxRefs) const { + if (nodeIndex < 0 || nodeIndex >= m_nodeCount) return currentCount; + if (currentCount >= maxRefs) + return currentCount; + + const BVHNode &node = m_nodes[nodeIndex]; + + if (!frustum.testAABB(node)) { + return currentCount; + } + + if (node.isLeaf()) { + int count = node.triangleCount; + int available = maxRefs - currentCount; + if (count > available) + count = available; + + for (int i = 0; i < count; i++) { + outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i]; + } + return currentCount + count; + } + + if (node.leftChild != 0xFFFF) { + currentCount = traverseFrustum(node.leftChild, frustum, outRefs, + currentCount, maxRefs); + } + if (node.rightChild != 0xFFFF) { + currentCount = traverseFrustum(node.rightChild, frustum, outRefs, + currentCount, maxRefs); + } + + return currentCount; } int BVHManager::queryRegion(int32_t minX, int32_t minY, int32_t minZ, - int32_t maxX, int32_t maxY, int32_t maxZ, - TriangleRef* outRefs, - int maxRefs) const { - if (!isLoaded() || m_nodeCount == 0) return 0; - - return traverseRegion(0, minX, minY, minZ, maxX, maxY, maxZ, outRefs, 0, maxRefs); + int32_t maxX, int32_t maxY, int32_t maxZ, + TriangleRef *outRefs, int maxRefs) const { + if (!isLoaded() || m_nodeCount == 0) + return 0; + + return traverseRegion(0, minX, minY, minZ, maxX, maxY, maxZ, outRefs, 0, + maxRefs); } -int BVHManager::traverseRegion(int nodeIndex, - int32_t qMinX, int32_t qMinY, int32_t qMinZ, - int32_t qMaxX, int32_t qMaxY, int32_t qMaxZ, - TriangleRef* outRefs, - int currentCount, - int maxRefs) const { - if (nodeIndex < 0 || nodeIndex >= m_nodeCount) return currentCount; - if (currentCount >= maxRefs) return currentCount; - - const BVHNode& node = m_nodes[nodeIndex]; - - // AABB overlap test - if (!aabbOverlap(node, qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ)) { - return currentCount; // No overlap, skip - } - - // If leaf, add all triangles +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]; + + if (!aabbOverlap(node, qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ)) { + return currentCount; + if (node.isLeaf()) { - int count = node.triangleCount; - int available = maxRefs - currentCount; - if (count > available) count = available; - - for (int i = 0; i < count; i++) { - outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i]; - } - return currentCount + count; + int count = node.triangleCount; + int available = maxRefs - currentCount; + if (count > available) + count = available; + + for (int i = 0; i < count; i++) { + outRefs[currentCount + i] = m_triangleRefs[node.firstTriangle + i]; + } + return currentCount + count; } - - // Recurse into children + if (node.leftChild != 0xFFFF) { - currentCount = traverseRegion(node.leftChild, - qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ, - outRefs, currentCount, maxRefs); + 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 +} // namespace psxsplash diff --git a/src/bvh.hh b/src/bvh.hh index 8214882..9564db1 100644 --- a/src/bvh.hh +++ b/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(reinterpret_cast(m_nodes) + delta); + if (m_triangleRefs) m_triangleRefs = reinterpret_cast(reinterpret_cast(m_triangleRefs) + delta); + } + private: const BVHNode* m_nodes = nullptr; const TriangleRef* m_triangleRefs = nullptr; diff --git a/src/camera.cpp b/src/camera.cpp index 2c19b80..02a643e 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -6,7 +6,6 @@ #include 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,17 +78,13 @@ 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; frustum.planes[3].d = -(((int64_t)frustum.planes[3].nx * camX + (int64_t)frustum.planes[3].ny * camY + (int64_t)frustum.planes[3].nz * camZ) >> 12); - - // PLANE 4: BOTTOM PLANE - cull things BELOW view - // Normal = up * H + forward * screenHalfHeight (points into frustum) + frustum.planes[4].nx = ((int64_t)upX * H + (int64_t)fwdX * SCREEN_HALF_HEIGHT) >> 12; frustum.planes[4].ny = ((int64_t)upY * H + (int64_t)fwdY * SCREEN_HALF_HEIGHT) >> 12; frustum.planes[4].nz = ((int64_t)upZ * H + (int64_t)fwdZ * SCREEN_HALF_HEIGHT) >> 12; @@ -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; diff --git a/src/camera.hh b/src/camera.hh index 996589f..87a3982 100644 --- a/src/camera.hh +++ b/src/camera.hh @@ -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; } diff --git a/src/collision.cpp b/src/collision.cpp index 0a23f82..187e3a0 100644 --- a/src/collision.cpp +++ b/src/collision.cpp @@ -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; } } diff --git a/src/collision.hh b/src/collision.hh index 2c535e0..21126db 100644 --- a/src/collision.hh +++ b/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 #include #include @@ -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, @@ -69,8 +44,7 @@ struct AABB { (min.z + max.z) / 2 }; } - - // Get half-extents + psyqo::Vec3 halfExtents() const { return psyqo::Vec3{ (max.x - min.x) / 2, @@ -78,162 +52,106 @@ struct AABB { (max.z - min.z) / 2 }; } - - // Expand AABB by a vector (for swept tests) + void expand(const psyqo::Vec3& delta); }; -static_assert(sizeof(AABB) == 24, "AABB must be 24 bytes (2x Vec3)"); +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 - uint8_t state; // 0=new, 1=staying, 2=exiting + 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; - + struct Cell { uint16_t objectIndices[MAX_OBJECTS_PER_CELL]; uint8_t count; uint8_t padding[3]; }; - - // Clear all cells + void clear(); - - // Insert an object into the grid void insert(uint16_t objectIndex, const AABB& bounds); - - // Get all potential colliders for an AABB - // Returns number of results written to output int queryAABB(const AABB& bounds, uint16_t* output, int maxResults) const; - - // Get cell index for a position int getCellIndex(const psyqo::Vec3& pos) const; - + private: Cell m_cells[CELL_COUNT]; - - // Convert world position to grid coordinates void worldToGrid(const psyqo::Vec3& pos, int& gx, int& gy, int& gz) const; }; -/** - * Main Collision System - * Manages all collision detection and trigger events - */ class CollisionSystem { public: static constexpr int MAX_COLLIDERS = 64; static constexpr int MAX_TRIGGERS = 32; static constexpr int MAX_COLLISION_RESULTS = 32; - + CollisionSystem() = default; - - // Initialize the system + void init(); - - // Reset for new scene void reset(); - - // Register a collider (called during scene load) - void registerCollider(uint16_t gameObjectIndex, const AABB& localBounds, + + 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, + + bool testAABB(const AABB& a, const AABB& b, psyqo::Vec3& normal, psyqo::FixedPoint<12>& penetration) const; - - // Update trigger state machine void updateTriggerState(uint16_t triggerIndex, uint16_t otherIndex, bool isOverlapping); }; -} // namespace psxsplash +} // namespace psxsplash \ No newline at end of file diff --git a/src/fileloader.cpp b/src/fileloader.cpp new file mode 100644 index 0000000..cf6cb7d --- /dev/null +++ b/src/fileloader.cpp @@ -0,0 +1,46 @@ +#include "fileloader.hh" +#include + +// ── 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 diff --git a/src/fileloader.hh b/src/fileloader.hh new file mode 100644 index 0000000..716ab19 --- /dev/null +++ b/src/fileloader.hh @@ -0,0 +1,116 @@ +#pragma once + +#include +#include + +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 diff --git a/src/fileloader_cdrom.hh b/src/fileloader_cdrom.hh new file mode 100644 index 0000000..ba4b684 --- /dev/null +++ b/src/fileloader_cdrom.hh @@ -0,0 +1,234 @@ +#pragma once + +#include "fileloader.hh" + +#include +#include +#include +#include + +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(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(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(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 diff --git a/src/fileloader_pcdrv.hh b/src/fileloader_pcdrv.hh new file mode 100644 index 0000000..6d3c957 --- /dev/null +++ b/src/fileloader_pcdrv.hh @@ -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 diff --git a/src/loadingscreen.cpp b/src/loadingscreen.cpp index 63cb435..76dd987 100644 --- a/src/loadingscreen.cpp +++ b/src/loadingscreen.cpp @@ -1,5 +1,5 @@ #include "loadingscreen.hh" -#include "sceneloader.hh" +#include "fileloader.hh" #include "renderer.hh" #include @@ -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(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)}})); diff --git a/src/main.cpp b/src/main.cpp index 3eca16f..4b51300 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,13 +4,16 @@ #include #include #include +#include #include #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::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; diff --git a/src/navregion.hh b/src/navregion.hh index e66329d..6e83cba 100644 --- a/src/navregion.hh +++ b/src/navregion.hh @@ -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(reinterpret_cast(m_regions) + delta); + if (m_portals) m_portals = reinterpret_cast(reinterpret_cast(m_portals) + delta); + } + /// Get the number of regions uint16_t getRegionCount() const { return m_header.regionCount; } diff --git a/src/sceneloader.cpp b/src/sceneloader.cpp index e5a23fd..f962cdc 100644 --- a/src/sceneloader.cpp +++ b/src/sceneloader.cpp @@ -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 diff --git a/src/sceneloader.hh b/src/sceneloader.hh index dca2a50..242adb6 100644 --- a/src/sceneloader.hh +++ b/src/sceneloader.hh @@ -5,45 +5,27 @@ namespace psxsplash { /** - * SceneLoader — loads splashpack files from PCdrv (emulator) or CD-ROM. - * - * In emulator (PCdrv) mode, files are loaded via the host filesystem using - * the PCdrv protocol (break instructions intercepted by PCSX-Redux). - * - * In CD-ROM mode (future), files would be loaded from the disc. - * - * The loader allocates memory for the file content and returns a pointer - * to the caller. The caller owns the memory. + * SceneLoader — backward-compatibility façade. + * + * All calls now delegate to the FileLoader singleton (selected at compile + * time by the LOADER build flag). New code should use FileLoader directly. */ class SceneLoader { public: - /** - * 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 diff --git a/src/scenemanager.cpp b/src/scenemanager.cpp index 5178aae..2a96fad 100644 --- a/src/scenemanager.cpp +++ b/src/scenemanager.cpp @@ -9,6 +9,8 @@ #include "luaapi.hh" #include "loadingscreen.hh" +#include + #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 — - // Lua VM, vector backing storage, audio. This returns as much - // heap memory as possible before any new allocation. - clearScene(); + 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. - if (m_currentSceneData) { - SceneLoader::FreeFile(m_currentSceneData); - m_currentSceneData = nullptr; + // Free old splashpack data BEFORE loading the new one. + // This avoids having both scene buffers in the heap simultaneously. + if (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(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(newBase) - reinterpret_cast(oldBase); + + auto reloc = [delta](auto* ptr) -> decltype(ptr) { + if (!ptr) return ptr; + return reinterpret_cast(reinterpret_cast(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 // ============================================================================ diff --git a/src/scenemanager.hh b/src/scenemanager.hh index c453563..b0fccbb 100644 --- a/src/scenemanager.hh +++ b/src/scenemanager.hh @@ -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 \ No newline at end of file diff --git a/src/splashpack.cpp b/src/splashpack.cpp index 68696b3..d8e81a0 100644 --- a/src/splashpack.cpp +++ b/src/splashpack.cpp @@ -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(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); - } + 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(cursor); cursor += header->bvhNodeCount * sizeof(BVHNode); @@ -169,58 +148,30 @@ void SplashPackLoader::LoadSplashpack(uint8_t *data, SplashpackSceneSetup &setup setup.bvh.initialize(bvhNodes, header->bvhNodeCount, triangleRefs, header->bvhTriangleRefCount); } - - // Read component data (version 4+) - if (header->version >= 4) { - // Interactables - for (uint16_t i = 0; i < header->interactableCount; i++) { - psxsplash::Interactable *interactable = reinterpret_cast(cursor); - setup.interactables.push_back(interactable); - cursor += sizeof(psxsplash::Interactable); - } - - // Skip health components (legacy, 24 bytes each) - cursor += header->healthCount * 24; - - // Skip timers (legacy, 16 bytes each) - cursor += header->timerCount * 16; - - // Skip spawners (legacy, 44 bytes each) - cursor += header->spawnerCount * 44; - } - - // Read NavGrid (version 5+ — LEGACY, skip if present) - if (header->version >= 5 && header->hasNavGrid) { - // Skip NavGrid data: header (16 bytes) + cells - // NavGridHeader: 4 int32 = 16 bytes, then gridW*gridH*9 bytes - int32_t* navGridHeader = reinterpret_cast(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(cursor); - cursor = reinterpret_cast((addr + 3) & ~3); + + // Interactables + for (uint16_t i = 0; i < header->interactableCount; i++) { + psxsplash::Interactable *interactable = reinterpret_cast(cursor); + setup.interactables.push_back(interactable); + cursor += sizeof(psxsplash::Interactable); } - // 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(cursor); cursor = reinterpret_cast((addr + 3) & ~3); cursor = const_cast(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(cursor); cursor = reinterpret_cast((addr + 3) & ~3); cursor = const_cast(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(cursor); cursor = reinterpret_cast((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(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.fogEnabled = header->fogEnabled != 0; + setup.fogR = header->fogR; + setup.fogG = header->fogG; + setup.fogB = header->fogB; + setup.fogDensity = header->fogDensity; 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 diff --git a/src/splashpack.hh b/src/splashpack.hh index 31880da..0621a89 100644 --- a/src/splashpack.hh +++ b/src/splashpack.hh @@ -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 { diff --git a/src/uisystem.cpp b/src/uisystem.cpp index dc2e12d..115452c 100644 --- a/src/uisystem.cpp +++ b/src/uisystem.cpp @@ -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(reinterpret_cast(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(reinterpret_cast(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; diff --git a/src/uisystem.hh b/src/uisystem.hh index f2c32c1..8df5a38 100644 --- a/src/uisystem.hh +++ b/src/uisystem.hh @@ -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, diff --git a/src/worldcollision.hh b/src/worldcollision.hh index 9efe224..9c99abd 100644 --- a/src/worldcollision.hh +++ b/src/worldcollision.hh @@ -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(reinterpret_cast(m_meshes) + delta); + if (m_triangles) m_triangles = reinterpret_cast(reinterpret_cast(m_triangles) + delta); + if (m_chunks) m_chunks = reinterpret_cast(reinterpret_cast(m_chunks) + delta); + } + // ======================================================================== // High-level queries used by the player movement system // ========================================================================