#include "scenemanager.hh" #include #include "collision.hh" #include "profiler.hh" #include "renderer.hh" #include "splashpack.hh" #include "luaapi.hh" #include "loadingscreen.hh" #include #include "lua.h" using namespace psyqo::trig_literals; using namespace psyqo::fixed_point_literals; using namespace psxsplash; // Static member definition psyqo::Font<>* psxsplash::SceneManager::s_font = nullptr; void psxsplash::SceneManager::InitializeScene(uint8_t* splashpackData, LoadingScreen* loading) { auto& gpu = Renderer::GetInstance().getGPU(); L.Reset(); // Initialize audio system m_audio.init(); // Register the Lua API LuaAPI::RegisterAll(L.getState(), this, &m_cutscenePlayer, &m_uiSystem); #ifdef PSXSPLASH_PROFILER debug::Profiler::getInstance().initialize(); #endif SplashpackSceneSetup sceneSetup; m_loader.LoadSplashpack(splashpackData, sceneSetup); if (loading && loading->isActive()) loading->updateProgress(gpu, 40); m_luaFiles = std::move(sceneSetup.luaFiles); m_gameObjects = std::move(sceneSetup.objects); m_objectNames = std::move(sceneSetup.objectNames); m_bvh = sceneSetup.bvh; // Copy BVH for frustum culling m_worldCollision = sceneSetup.worldCollision; // World collision soup (v7+) m_navRegions = sceneSetup.navRegions; // Nav region system (v7+) m_playerNavRegion = m_navRegions.isLoaded() ? m_navRegions.getStartRegion() : NAV_NO_REGION; // Scene type and render path m_sceneType = sceneSetup.sceneType; // Room/portal data for interior scenes (v11+) m_rooms = sceneSetup.rooms; m_roomCount = sceneSetup.roomCount; m_portals = sceneSetup.portals; m_portalCount = sceneSetup.portalCount; m_roomTriRefs = sceneSetup.roomTriRefs; m_roomTriRefCount = sceneSetup.roomTriRefCount; // Configure fog from splashpack data (v11+) if (sceneSetup.fogEnabled) { psxsplash::FogConfig fogCfg; fogCfg.enabled = true; fogCfg.color = {.r = sceneSetup.fogR, .g = sceneSetup.fogG, .b = sceneSetup.fogB}; fogCfg.density = sceneSetup.fogDensity; Renderer::GetInstance().SetFog(fogCfg); } else { psxsplash::FogConfig fogCfg; fogCfg.enabled = false; Renderer::GetInstance().SetFog(fogCfg); } // Copy component arrays m_interactables = std::move(sceneSetup.interactables); // Load audio clips into SPU RAM m_audioClipNames = std::move(sceneSetup.audioClipNames); for (size_t i = 0; i < sceneSetup.audioClips.size(); i++) { auto& clip = sceneSetup.audioClips[i]; m_audio.loadClip((int)i, clip.adpcmData, clip.sizeBytes, clip.sampleRate, clip.loop); } if (loading && loading->isActive()) loading->updateProgress(gpu, 55); // Copy cutscene data into scene manager storage (sceneSetup is stack-local) m_cutsceneCount = sceneSetup.cutsceneCount; for (int i = 0; i < m_cutsceneCount; i++) { m_cutscenes[i] = sceneSetup.loadedCutscenes[i]; } // Initialize cutscene player (v12+) m_cutscenePlayer.init( m_cutsceneCount > 0 ? m_cutscenes : nullptr, m_cutsceneCount, &m_currentCamera, &m_audio, &m_uiSystem ); // Initialize UI system (v13+) if (sceneSetup.uiCanvasCount > 0 && sceneSetup.uiTableOffset != 0 && s_font != nullptr) { m_uiSystem.init(*s_font); m_uiSystem.loadFromSplashpack(splashpackData, sceneSetup.uiCanvasCount, sceneSetup.uiFontCount, sceneSetup.uiTableOffset); m_uiSystem.uploadFonts(Renderer::GetInstance().getGPU()); Renderer::GetInstance().SetUISystem(&m_uiSystem); if (loading && loading->isActive()) loading->updateProgress(gpu, 70); // Resolve UI track handles: the splashpack loader stored raw name pointers // in CutsceneTrack.target for UI tracks. Now that UISystem is loaded, resolve // those names to canvas indices / element handles. for (int ci = 0; ci < m_cutsceneCount; ci++) { for (uint8_t ti = 0; ti < m_cutscenes[ci].trackCount; ti++) { auto& track = m_cutscenes[ci].tracks[ti]; bool isUI = static_cast(track.trackType) >= 5; if (!isUI || track.target == nullptr) continue; const char* nameStr = reinterpret_cast(track.target); track.target = nullptr; // Clear the temporary name pointer if (track.trackType == TrackType::UICanvasVisible) { // Name is just the canvas name track.uiHandle = static_cast(m_uiSystem.findCanvas(nameStr)); } else { // Name is "canvasName/elementName" — find the '/' separator const char* sep = nameStr; while (*sep && *sep != '/') sep++; if (*sep == '/') { // Temporarily null-terminate the canvas portion // (nameStr points into splashpack data, which is mutable) char* mutableSep = const_cast(sep); *mutableSep = '\0'; int canvasIdx = m_uiSystem.findCanvas(nameStr); *mutableSep = '/'; // Restore the separator if (canvasIdx >= 0) { track.uiHandle = static_cast( m_uiSystem.findElement(canvasIdx, sep + 1)); } } } } } } else { Renderer::GetInstance().SetUISystem(nullptr); } m_playerPosition = sceneSetup.playerStartPosition; playerRotationX = 0.0_pi; playerRotationY = 0.0_pi; playerRotationZ = 0.0_pi; m_playerHeight = sceneSetup.playerHeight; // Load movement parameters from splashpack (v8+) m_controls.setMoveSpeed(sceneSetup.moveSpeed); m_controls.setSprintSpeed(sceneSetup.sprintSpeed); m_playerRadius = (int32_t)sceneSetup.playerRadius.value; if (m_playerRadius == 0) m_playerRadius = PLAYER_RADIUS; // fallback to default m_jumpVelocityRaw = (int32_t)sceneSetup.jumpVelocity.value; int32_t gravityRaw = (int32_t)sceneSetup.gravity.value; m_gravityPerFrame = gravityRaw / 30; // Convert per-second² to per-frame velocity change if (m_gravityPerFrame == 0 && gravityRaw > 0) m_gravityPerFrame = 1; // Ensure nonzero m_velocityY = 0; m_isGrounded = true; m_lastFrameTime = 0; m_deltaFrames = 1; // Initialize collision system m_collisionSystem.init(); // Register colliders from splashpack data for (size_t i = 0; i < sceneSetup.colliders.size(); i++) { SPLASHPACKCollider* collider = sceneSetup.colliders[i]; if (collider == nullptr) continue; AABB bounds; bounds.min.x.value = collider->minX; bounds.min.y.value = collider->minY; bounds.min.z.value = collider->minZ; bounds.max.x.value = collider->maxX; bounds.max.y.value = collider->maxY; bounds.max.z.value = collider->maxZ; CollisionType type = static_cast(collider->collisionType); m_collisionSystem.registerCollider( collider->gameObjectIndex, bounds, type, collider->layerMask ); } // Register trigger boxes from splashpack data for (size_t i = 0; i < sceneSetup.triggerBoxes.size(); i++) { SPLASHPACKTriggerBox* tb = sceneSetup.triggerBoxes[i]; if (tb == nullptr) continue; AABB bounds; bounds.min.x.value = tb->minX; bounds.min.y.value = tb->minY; bounds.min.z.value = tb->minZ; bounds.max.x.value = tb->maxX; bounds.max.y.value = tb->maxY; bounds.max.z.value = tb->maxZ; m_collisionSystem.registerTriggerBox(bounds, tb->luaFileIndex); } // Load Lua files - order is important here. We need // to load the Lua files before we register the game objects, // as the game objects may reference Lua files by index. for (int i = 0; i < m_luaFiles.size(); i++) { auto luaFile = m_luaFiles[i]; L.LoadLuaFile(luaFile->luaCode, luaFile->length, i); } if (loading && loading->isActive()) loading->updateProgress(gpu, 85); L.RegisterSceneScripts(sceneSetup.sceneLuaFileIndex); L.OnSceneCreationStart(); // Register game objects for (auto object : m_gameObjects) { L.RegisterGameObject(object); } m_controls.forceAnalogMode(); m_controls.Init(); Renderer::GetInstance().SetCamera(m_currentCamera); L.OnSceneCreationEnd(); if (loading && loading->isActive()) loading->updateProgress(gpu, 95); m_liveDataSize = sceneSetup.liveDataSize; shrinkBuffer(); if (loading && loading->isActive()) loading->updateProgress(gpu, 100); } void psxsplash::SceneManager::GameTick(psyqo::GPU &gpu) { LuaAPI::IncrementFrameCount(); // Tick cutscene player (advance frame and apply tracks before rendering) m_cutscenePlayer.tick(); // Delta-time measurement: count elapsed frames based on gpu timer // PS1 NTSC frame = ~33333 microseconds (30fps vsync) { uint32_t now = gpu.now(); if (m_lastFrameTime != 0) { uint32_t elapsed = now - m_lastFrameTime; // 33333us per frame at 30fps. If >50000us, we dropped a frame. m_deltaFrames = (elapsed > 50000) ? 2 : 1; if (elapsed > 83000) m_deltaFrames = 3; // Two frames dropped } m_lastFrameTime = now; } uint32_t renderingStart = gpu.now(); auto& renderer = psxsplash::Renderer::GetInstance(); // Dispatch render path based on scene type. // Interior scenes (type 1) use room/portal occlusion; exterior scenes use BVH culling. if (m_sceneType == 1 && m_roomCount > 0 && m_rooms != nullptr) { // Determine which room the camera is in for portal culling. // During cutscene playback the camera may be elsewhere from the player, // so use the actual camera position to find the room. int camRoom = -1; if (m_navRegions.isLoaded()) { if (m_cutscenePlayer.isPlaying()) { // Camera-driven: look up nav region from camera world position auto& camPos = m_currentCamera.GetPosition(); uint16_t camRegion = m_navRegions.findRegion(camPos.x.value, camPos.z.value); if (camRegion != NAV_NO_REGION) { uint8_t ri = m_navRegions.getRoomIndex(camRegion); if (ri != 0xFF) camRoom = (int)ri; } } else if (m_playerNavRegion != NAV_NO_REGION) { // Normal gameplay: use cached player nav region uint8_t ri = m_navRegions.getRoomIndex(m_playerNavRegion); if (ri != 0xFF) camRoom = (int)ri; } } renderer.RenderWithRooms(m_gameObjects, m_rooms, m_roomCount, m_portals, m_portalCount, m_roomTriRefs, camRoom); } else { renderer.RenderWithBVH(m_gameObjects, m_bvh); } gpu.pumpCallbacks(); uint32_t renderingEnd = gpu.now(); uint32_t renderingTime = renderingEnd - renderingStart; #ifdef PSXSPLASH_PROFILER psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_RENDERING, renderingTime); #endif // Collision detection uint32_t collisionStart = gpu.now(); // Build player AABB from position + radius/height AABB playerAABB; { psyqo::FixedPoint<12> r; r.value = m_playerRadius; psyqo::FixedPoint<12> px = static_cast>(m_playerPosition.x); psyqo::FixedPoint<12> py = static_cast>(m_playerPosition.y); psyqo::FixedPoint<12> pz = static_cast>(m_playerPosition.z); psyqo::FixedPoint<12> h = static_cast>(m_playerHeight); playerAABB.min = psyqo::Vec3{px - r, py - h, pz - r}; playerAABB.max = psyqo::Vec3{px + r, py, pz + r}; } psyqo::Vec3 pushBack; int collisionCount = m_collisionSystem.detectCollisions(playerAABB, pushBack); // Apply push-back to player position { psyqo::FixedPoint<12> zero; if (pushBack.x != zero || pushBack.z != zero) { m_playerPosition.x = m_playerPosition.x + pushBack.x; m_playerPosition.z = m_playerPosition.z + pushBack.z; } } // Fire onCollideWithPlayer Lua events on collided objects const CollisionResult* results = m_collisionSystem.getResults(); for (int i = 0; i < collisionCount; i++) { if (results[i].objectA != 0xFFFF) continue; auto* obj = getGameObject(results[i].objectB); if (obj) { L.OnCollideWithPlayer(obj); } } // Process trigger boxes (enter/exit) m_collisionSystem.detectTriggers(playerAABB, *this); gpu.pumpCallbacks(); uint32_t collisionEnd = gpu.now(); uint32_t luaStart = gpu.now(); // Lua update tick - call onUpdate for all registered objects with onUpdate handler for (auto* go : m_gameObjects) { if (go && go->isActive()) { L.OnUpdate(go, m_deltaFrames); } } gpu.pumpCallbacks(); uint32_t luaEnd = gpu.now(); uint32_t luaTime = luaEnd - luaStart; #ifdef PSXSPLASH_PROFILER psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_LUA, luaTime); #endif // Update game systems processEnableDisableEvents(); uint32_t controlsStart = gpu.now(); // Update button state tracking first m_controls.UpdateButtonStates(); // Update interaction system (checks for interact button press) updateInteractionSystem(); // Dispatch button events to all objects uint16_t pressed = m_controls.getButtonsPressed(); uint16_t released = m_controls.getButtonsReleased(); if (pressed || released) { // Only iterate objects if there are button events for (auto* go : m_gameObjects) { if (!go || !go->isActive()) continue; if (pressed) { // Dispatch press events for each pressed button for (int btn = 0; btn < 16; btn++) { if (pressed & (1 << btn)) { L.OnButtonPress(go, btn); } } } if (released) { // Dispatch release events for each released button for (int btn = 0; btn < 16; btn++) { if (released & (1 << btn)) { L.OnButtonRelease(go, btn); } } } } } // Save position BEFORE movement for collision detection psyqo::Vec3 oldPlayerPosition = m_playerPosition; m_controls.HandleControls(m_playerPosition, playerRotationX, playerRotationY, playerRotationZ, freecam, m_deltaFrames); // Jump input: Cross button triggers jump when grounded if (m_isGrounded && m_controls.wasButtonPressed(psyqo::AdvancedPad::Button::Cross)) { m_velocityY = -m_jumpVelocityRaw; // Negative = upward (PSX Y-down) m_isGrounded = false; } gpu.pumpCallbacks(); uint32_t controlsEnd = gpu.now(); uint32_t controlsTime = controlsEnd - controlsStart; #ifdef PSXSPLASH_PROFILER psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_CONTROLS, controlsTime); #endif uint32_t navmeshStart = gpu.now(); if (!freecam) { // Priority: WorldCollision + NavRegions (v7) > NavGrid (v5) > Legacy Navmesh if (m_worldCollision.isLoaded()) { // Move-and-slide against world geometry (XZ walls only) psyqo::Vec3 slid = m_worldCollision.moveAndSlide( oldPlayerPosition, m_playerPosition, m_playerRadius, 0xFF); m_playerPosition.x = slid.x; m_playerPosition.z = slid.z; // Apply gravity: velocity changes each frame for (int f = 0; f < m_deltaFrames; f++) { m_velocityY += m_gravityPerFrame; } // Apply vertical velocity to position // velocityY is in fp12 per-second; convert per-frame: pos += vel / 30 int32_t posYDelta = (m_velocityY * m_deltaFrames) / 30; m_playerPosition.y.value += posYDelta; // Resolve floor Y from nav regions if available if (m_navRegions.isLoaded()) { uint16_t prevRegion = m_playerNavRegion; int32_t px = m_playerPosition.x.value; int32_t pz = m_playerPosition.z.value; int32_t floorY = m_navRegions.resolvePosition( px, pz, m_playerNavRegion); if (m_playerNavRegion != NAV_NO_REGION) { m_playerPosition.x.value = px; m_playerPosition.z.value = pz; // Ground (feet) position in PSX coords: // Camera is at position.y, feet are at position.y + playerHeight // (Y-down: larger Y = lower) int32_t cameraAtFloor = floorY - m_playerHeight.raw(); if (m_playerPosition.y.value >= cameraAtFloor) { // Player is at or below floor — snap to ground m_playerPosition.y.value = cameraAtFloor; m_velocityY = 0; m_isGrounded = true; } else { // Player is above floor (jumping/airborne) m_isGrounded = false; } } else { // Off all nav regions — revert to old position m_playerPosition = oldPlayerPosition; m_playerNavRegion = prevRegion; m_velocityY = 0; m_isGrounded = true; } } else { // Ground trace fallback (no nav regions) int32_t groundY; int32_t groundNormalY; uint8_t surfFlags; if (m_worldCollision.groundTrace(m_playerPosition, 4096 * 4, // max 4 units down groundY, groundNormalY, surfFlags, 0xFF)) { int32_t cameraAtFloor = groundY - m_playerHeight.raw(); if (m_playerPosition.y.value >= cameraAtFloor) { m_playerPosition.y.value = cameraAtFloor; m_velocityY = 0; m_isGrounded = true; } else { m_isGrounded = false; } } else { m_playerPosition = oldPlayerPosition; m_velocityY = 0; m_isGrounded = true; } } // Ceiling check: if jumping upward, check for ceiling collision if (m_velocityY < 0 && m_worldCollision.isLoaded()) { int32_t ceilingY; if (m_worldCollision.ceilingTrace(m_playerPosition, m_playerHeight.raw(), ceilingY, 0xFF)) { // Hit a ceiling — stop upward velocity m_velocityY = 0; } } } } gpu.pumpCallbacks(); uint32_t navmeshEnd = gpu.now(); uint32_t navmeshTime = navmeshEnd - navmeshStart; #ifdef PSXSPLASH_PROFILER psxsplash::debug::Profiler::getInstance().setSectionTime(psxsplash::debug::PROFILER_NAVMESH, navmeshTime); #endif m_currentCamera.SetPosition(static_cast>(m_playerPosition.x), static_cast>(m_playerPosition.y), static_cast>(m_playerPosition.z)); m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ); // Process pending scene transitions (at end of frame) processPendingSceneLoad(); } void psxsplash::SceneManager::fireTriggerEnter(int16_t luaFileIndex, uint16_t triggerIndex) { if (luaFileIndex < 0) return; L.OnTriggerEnterScript(luaFileIndex, triggerIndex); } void psxsplash::SceneManager::fireTriggerExit(int16_t luaFileIndex, uint16_t triggerIndex) { if (luaFileIndex < 0) return; L.OnTriggerExitScript(luaFileIndex, triggerIndex); } // ============================================================================ // INTERACTION SYSTEM // ============================================================================ void psxsplash::SceneManager::updateInteractionSystem() { // Get interact button state - Cross button by default auto interactButton = psyqo::AdvancedPad::Button::Cross; bool buttonPressed = m_controls.wasButtonPressed(interactButton); if (!buttonPressed) return; // Early out if no interaction attempt // Player position for distance check psyqo::FixedPoint<12> playerX = static_cast>(m_playerPosition.x); psyqo::FixedPoint<12> playerY = static_cast>(m_playerPosition.y); psyqo::FixedPoint<12> playerZ = static_cast>(m_playerPosition.z); // Find closest interactable in range Interactable* closest = nullptr; psyqo::FixedPoint<12> closestDistSq; closestDistSq.value = 0x7FFFFFFF; // Max positive value for (auto* interactable : m_interactables) { if (!interactable || !interactable->canInteract()) continue; // Check if object is active auto* go = getGameObject(interactable->gameObjectIndex); if (!go || !go->isActive()) continue; // Calculate distance squared psyqo::FixedPoint<12> dx = playerX - interactable->offsetX - go->position.x; psyqo::FixedPoint<12> dy = playerY - interactable->offsetY - go->position.y; psyqo::FixedPoint<12> dz = playerZ - interactable->offsetZ - go->position.z; psyqo::FixedPoint<12> distSq = dx * dx + dy * dy + dz * dz; // Check if in range and closer than current closest if (distSq <= interactable->radiusSquared && distSq < closestDistSq) { closest = interactable; closestDistSq = distSq; } } // Interact with closest if (closest != nullptr) { triggerInteraction(getGameObject(closest->gameObjectIndex)); closest->triggerCooldown(); } } void psxsplash::SceneManager::triggerInteraction(GameObject* interactable) { if (!interactable) return; L.OnInteract(interactable); } // ============================================================================ // ENABLE/DISABLE SYSTEM // ============================================================================ void psxsplash::SceneManager::setObjectActive(GameObject* go, bool active) { if (!go) return; bool wasActive = go->isActive(); if (wasActive == active) return; // No change go->setActive(active); // Fire appropriate event if (active) { L.OnEnable(go); } else { L.OnDisable(go); } } void psxsplash::SceneManager::processEnableDisableEvents() { // Process any pending enable/disable flags (for batched operations) for (auto* go : m_gameObjects) { if (!go) continue; if (go->isPendingEnable()) { go->setPendingEnable(false); if (!go->isActive()) { go->setActive(true); L.OnEnable(go); } } if (go->isPendingDisable()) { go->setPendingDisable(false); if (go->isActive()) { go->setActive(false); L.OnDisable(go); } } } } // ============================================================================ // SCENE LOADING // ============================================================================ void psxsplash::SceneManager::requestSceneLoad(int sceneIndex) { if (sceneIndex == m_currentSceneIndex) return; m_pendingSceneIndex = sceneIndex; } void psxsplash::SceneManager::processPendingSceneLoad() { if (m_pendingSceneIndex < 0) return; int targetIndex = m_pendingSceneIndex; m_pendingSceneIndex = -1; auto& gpu = Renderer::GetInstance().getGPU(); loadScene(gpu, targetIndex, /*isFirstScene=*/false); } void psxsplash::SceneManager::loadScene(psyqo::GPU& gpu, int sceneIndex, bool isFirstScene) { // Build filename using the active backend's naming convention char filename[32]; FileLoader::BuildSceneFilename(sceneIndex, filename, sizeof(filename)); // Blank BOTH framebuffers so the user doesn't see the BIOS screen // or a frozen frame. FastFill ignores the scissor/DrawingArea so we // can target any VRAM region directly. psyqo::Prim::FastFill ff(psyqo::Color{.r = 0, .g = 0, .b = 0}); ff.rect = psyqo::Rect{0, 0, 320, 240}; gpu.sendPrimitive(ff); 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, sceneIndex)) { loading.renderInitialAndFree(gpu); } } 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(); // 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); // 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 = 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; } if (loading.isActive()) loading.updateProgress(gpu, 30); m_currentSceneData = newData; m_currentSceneIndex = sceneIndex; // Initialize with new data (creates fresh Lua VM inside) InitializeScene(newData, loading.isActive() ? &loading : nullptr); } void psxsplash::SceneManager::clearScene() { // 1. Shut down the Lua VM first — frees ALL Lua-allocated memory // (bytecode, strings, tables, registry) in one shot via lua_close. L.Shutdown(); // 2. Free vector BACKING STORAGE (not just contents). // clear() only sets size=0 but keeps the allocated capacity. // swap-with-empty releases the heap blocks so they can be coalesced. { eastl::vector tmp; tmp.swap(m_gameObjects); } { eastl::vector tmp; tmp.swap(m_luaFiles); } { eastl::vector tmp; tmp.swap(m_objectNames); } { eastl::vector tmp; tmp.swap(m_audioClipNames); } { eastl::vector tmp; tmp.swap(m_interactables); } // 3. Reset hardware / subsystems m_audio.reset(); // Free SPU RAM and stop all voices m_collisionSystem.init(); // Re-init collision system m_cutsceneCount = 0; m_cutscenePlayer.init(nullptr, 0, nullptr, nullptr); // Reset cutscene player // BVH, WorldCollision, and NavRegions will be overwritten by next load // Reset UI system (disconnect from renderer before splashpack data disappears) Renderer::GetInstance().SetUISystem(nullptr); // Reset room/portal pointers (they point into splashpack data which is being freed) m_rooms = nullptr; m_roomCount = 0; m_portals = nullptr; m_portalCount = 0; m_roomTriRefs = nullptr; m_roomTriRefCount = 0; m_sceneType = 0; } void psxsplash::SceneManager::shrinkBuffer() { if (m_liveDataSize == 0 || m_currentSceneData == nullptr) return; uint8_t* oldBase = m_currentSceneData; // Allocate the shrunk buffer. The volatile cast prevents the compiler // from assuming operator new never returns NULL (it does with // -fno-exceptions), which would let it optimize away the null check. uint8_t* volatile newBaseV = new uint8_t[m_liveDataSize]; uint8_t* newBase = newBaseV; if (!newBase) return; // Heap exhausted — keep the full buffer __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); // Re-key Lua registry entries for game objects. RegisterGameObject stored // lightuserdata keys using the OLD buffer addresses. After relocation, the // game object pointers changed but the registry keys are stale. Without // this, Entity.Find/FindByIndex return nil (wrong key), and in release // builds (-Os) the optimizer can turn this into a hard crash. if (!m_gameObjects.empty()) { L.RelocateGameObjects( reinterpret_cast(m_gameObjects.data()), m_gameObjects.size(), delta); } FileLoader::Get().FreeFile(oldBase); m_currentSceneData = newBase; } // ============================================================================ // OBJECT NAME LOOKUP // ============================================================================ // Inline streq (no libc on bare-metal PS1) static bool name_eq(const char* a, const char* b) { while (*a && *b) { if (*a++ != *b++) return false; } return *a == *b; } psxsplash::GameObject* psxsplash::SceneManager::findObjectByName(const char* name) const { if (!name || m_objectNames.empty()) return nullptr; for (size_t i = 0; i < m_objectNames.size() && i < m_gameObjects.size(); i++) { if (m_objectNames[i] && name_eq(m_objectNames[i], name)) { return m_gameObjects[i]; } } return nullptr; } int psxsplash::SceneManager::findAudioClipByName(const char* name) const { if (!name || m_audioClipNames.empty()) return -1; for (size_t i = 0; i < m_audioClipNames.size(); i++) { if (m_audioClipNames[i] && name_eq(m_audioClipNames[i], name)) { return static_cast(i); } } return -1; }