Files
secretpsxsplash/src/scenemanager.cpp
2026-03-27 16:39:10 +01:00

851 lines
32 KiB
C++

#include "scenemanager.hh"
#include <utility>
#include "collision.hh"
#include "profiler.hh"
#include "renderer.hh"
#include "splashpack.hh"
#include "luaapi.hh"
#include "loadingscreen.hh"
#include <psyqo/primitives/misc.hh>
#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<uint8_t>(track.trackType) >= 5;
if (!isUI || track.target == nullptr) continue;
const char* nameStr = reinterpret_cast<const char*>(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<int16_t>(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<char*>(sep);
*mutableSep = '\0';
int canvasIdx = m_uiSystem.findCanvas(nameStr);
*mutableSep = '/'; // Restore the separator
if (canvasIdx >= 0) {
track.uiHandle = static_cast<int16_t>(
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<CollisionType>(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.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<psyqo::FixedPoint<12>>(m_playerPosition.x);
psyqo::FixedPoint<12> py = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y);
psyqo::FixedPoint<12> pz = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z);
psyqo::FixedPoint<12> h = static_cast<psyqo::FixedPoint<12>>(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<psyqo::FixedPoint<12>>(m_playerPosition.x),
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y),
static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z));
m_currentCamera.SetRotation(playerRotationX, playerRotationY, playerRotationZ);
// Process pending scene transitions (at end of frame)
processPendingSceneLoad();
}
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<psyqo::FixedPoint<12>>(m_playerPosition.x);
psyqo::FixedPoint<12> playerY = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.y);
psyqo::FixedPoint<12> playerZ = static_cast<psyqo::FixedPoint<12>>(m_playerPosition.z);
// Find closest interactable in range
Interactable* closest = nullptr;
psyqo::FixedPoint<12> closestDistSq;
closestDistSq.value = 0x7FFFFFFF; // Max positive value
for (auto* interactable : m_interactables) {
if (!interactable || !interactable->canInteract()) continue;
// Check if object is active
auto* go = getGameObject(interactable->gameObjectIndex);
if (!go || !go->isActive()) continue;
// Calculate distance squared
psyqo::FixedPoint<12> dx = playerX - interactable->offsetX - go->position.x;
psyqo::FixedPoint<12> dy = playerY - interactable->offsetY - go->position.y;
psyqo::FixedPoint<12> dz = playerZ - interactable->offsetZ - go->position.z;
psyqo::FixedPoint<12> distSq = dx * dx + dy * dy + dz * dz;
// Check if in range and closer than current closest
if (distSq <= interactable->radiusSquared && distSq < closestDistSq) {
closest = interactable;
closestDistSq = distSq;
}
}
// Interact with closest
if (closest != nullptr) {
triggerInteraction(getGameObject(closest->gameObjectIndex));
closest->triggerCooldown();
}
}
void psxsplash::SceneManager::triggerInteraction(GameObject* interactable) {
if (!interactable) return;
L.OnInteract(interactable);
}
// ============================================================================
// ENABLE/DISABLE SYSTEM
// ============================================================================
void psxsplash::SceneManager::setObjectActive(GameObject* go, bool active) {
if (!go) return;
bool wasActive = go->isActive();
if (wasActive == active) return; // No change
go->setActive(active);
// Fire appropriate event
if (active) {
L.OnEnable(go);
} else {
L.OnDisable(go);
}
}
void psxsplash::SceneManager::processEnableDisableEvents() {
// Process any pending enable/disable flags (for batched operations)
for (auto* go : m_gameObjects) {
if (!go) continue;
if (go->isPendingEnable()) {
go->setPendingEnable(false);
if (!go->isActive()) {
go->setActive(true);
L.OnEnable(go);
}
}
if (go->isPendingDisable()) {
go->setPendingDisable(false);
if (go->isActive()) {
go->setActive(false);
L.OnDisable(go);
}
}
}
}
// ============================================================================
// SCENE LOADING
// ============================================================================
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<Ctx*>(ud);
c->ls->updateProgress(*c->gpu, pct);
},
&ctx, 20, 30
};
newData = FileLoader::Get().LoadFileSyncWithProgress(
filename, fileSize, &progress);
} else {
newData = FileLoader::Get().LoadFileSync(filename, fileSize);
}
if (!newData && isFirstScene) {
// Fallback: try legacy name for backwards compatibility (PCdrv only)
newData = FileLoader::Get().LoadFileSync("output.bin", fileSize);
}
if (!newData) {
return;
}
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<GameObject*> tmp; tmp.swap(m_gameObjects); }
{ eastl::vector<LuaFile*> tmp; tmp.swap(m_luaFiles); }
{ eastl::vector<const char*> tmp; tmp.swap(m_objectNames); }
{ eastl::vector<const char*> tmp; tmp.swap(m_audioClipNames); }
{ eastl::vector<Interactable*> tmp; tmp.swap(m_interactables); }
// 3. Reset hardware / subsystems
m_audio.reset(); // Free SPU RAM and stop all voices
m_collisionSystem.init(); // Re-init collision system
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<intptr_t>(newBase) - reinterpret_cast<intptr_t>(oldBase);
auto reloc = [delta](auto* ptr) -> decltype(ptr) {
if (!ptr) return ptr;
return reinterpret_cast<decltype(ptr)>(reinterpret_cast<intptr_t>(ptr) + delta);
};
for (auto& go : m_gameObjects) {
go = reloc(go);
go->polygons = reloc(go->polygons);
}
for (auto& lf : m_luaFiles) {
lf = reloc(lf);
lf->luaCode = reloc(lf->luaCode);
}
for (auto& name : m_objectNames) name = reloc(name);
for (auto& name : m_audioClipNames) name = reloc(name);
for (auto& inter : m_interactables) inter = reloc(inter);
m_bvh.relocate(delta);
m_worldCollision.relocate(delta);
m_navRegions.relocate(delta);
m_rooms = reloc(m_rooms);
m_portals = reloc(m_portals);
m_roomTriRefs = reloc(m_roomTriRefs);
for (int ci = 0; ci < m_cutsceneCount; ci++) {
auto& cs = m_cutscenes[ci];
cs.name = reloc(cs.name);
cs.audioEvents = reloc(cs.audioEvents);
for (uint8_t ti = 0; ti < cs.trackCount; ti++) {
auto& track = cs.tracks[ti];
track.keyframes = reloc(track.keyframes);
if (track.target) track.target = reloc(track.target);
}
}
m_uiSystem.relocate(delta);
FileLoader::Get().FreeFile(oldBase);
m_currentSceneData = newBase;
}
// ============================================================================
// OBJECT NAME LOOKUP
// ============================================================================
// Inline streq (no libc on bare-metal PS1)
static bool name_eq(const char* a, const char* b) {
while (*a && *b) { if (*a++ != *b++) return false; }
return *a == *b;
}
psxsplash::GameObject* psxsplash::SceneManager::findObjectByName(const char* name) const {
if (!name || m_objectNames.empty()) return nullptr;
for (size_t i = 0; i < m_objectNames.size() && i < m_gameObjects.size(); i++) {
if (m_objectNames[i] && name_eq(m_objectNames[i], name)) {
return m_gameObjects[i];
}
}
return nullptr;
}
int psxsplash::SceneManager::findAudioClipByName(const char* name) const {
if (!name || m_audioClipNames.empty()) return -1;
for (size_t i = 0; i < m_audioClipNames.size(); i++) {
if (m_audioClipNames[i] && name_eq(m_audioClipNames[i], name)) {
return static_cast<int>(i);
}
}
return -1;
}