Fix shutdown hangs, bank bag icons/drag-drop, loading screen progress, and login spawn

- Fix shutdown hang: skip vmaDestroyAllocator (walked thousands of allocations),
  replace unsafe pthread_timedjoin_np with plain join + early-exit checks in workers
- Bank window: full icon rendering, click-and-hold pickup (0.10s), drag-drop for
  all bank slots including bank bag equip slots, same-slot drop detection
- Loading screen: process one tile per frame for live progress updates
- Camera reset: trust server position in online mode to avoid spawning under WMOs
- Fix PLAYER_BYTES/PLAYER_BYTES_2 field indices, preserve purchasedBankBagSlots
  across inventory rebuilds, fix bank slot purchase result codes
This commit is contained in:
Kelsi 2026-02-26 13:38:29 -08:00
parent 804b947203
commit a559d5944b
14 changed files with 489 additions and 146 deletions

View file

@ -1756,15 +1756,47 @@ void CameraController::reset() {
return h;
};
// In online mode, try to snap to a nearby floor but fall back to the server
// position when no WMO floor is found (e.g. WMO not loaded yet in cities).
// This prevents spawning under WMO cities like Stormwind.
if (onlineMode) {
auto h = evalFloorAt(spawnPos.x, spawnPos.y, spawnPos.z);
if (h && std::abs(*h - spawnPos.z) < 16.0f) {
spawnPos.z = *h + 0.05f;
}
// else: keep server Z as-is
lastGroundZ = spawnPos.z - 0.05f;
camera->setRotation(yaw, pitch);
glm::vec3 forward3D = camera->getForward();
if (thirdPerson && followTarget) {
*followTarget = spawnPos;
currentDistance = userTargetDistance;
collisionDistance = currentDistance;
float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f;
glm::vec3 pivot = spawnPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset);
glm::vec3 camDir = -forward3D;
glm::vec3 camPos = pivot + camDir * currentDistance;
smoothedCamPos = camPos;
camera->setPosition(camPos);
} else {
spawnPos.z += eyeHeight;
smoothedCamPos = spawnPos;
camera->setPosition(spawnPos);
}
LOG_INFO("Camera reset to server position (online mode)");
return;
}
// Search nearby for a stable, non-steep spawn floor to avoid waterfall/ledge spawns.
// In online mode, use a tight search radius since the server dictates position.
float bestScore = std::numeric_limits<float>::max();
glm::vec3 bestPos = spawnPos;
bool foundBest = false;
constexpr float radiiOffline[] = {0.0f, 6.0f, 12.0f, 18.0f, 24.0f, 32.0f};
constexpr float radiiOnline[] = {0.0f, 2.0f};
const float* radii = onlineMode ? radiiOnline : radiiOffline;
const int radiiCount = onlineMode ? 2 : 6;
const float* radii = radiiOffline;
const int radiiCount = 6;
constexpr int ANGLES = 16;
constexpr float PI = 3.14159265f;
for (int ri = 0; ri < radiiCount; ri++) {

View file

@ -726,31 +726,38 @@ bool Renderer::initialize(core::Window* win) {
}
void Renderer::shutdown() {
LOG_WARNING("Renderer::shutdown - terrainManager stopWorkers...");
if (terrainManager) {
terrainManager->unloadAll();
terrainManager->stopWorkers();
LOG_WARNING("Renderer::shutdown - terrainManager reset...");
terrainManager.reset();
}
LOG_WARNING("Renderer::shutdown - terrainRenderer...");
if (terrainRenderer) {
terrainRenderer->shutdown();
terrainRenderer.reset();
}
LOG_WARNING("Renderer::shutdown - waterRenderer...");
if (waterRenderer) {
waterRenderer->shutdown();
waterRenderer.reset();
}
LOG_WARNING("Renderer::shutdown - minimap...");
if (minimap) {
minimap->shutdown();
minimap.reset();
}
LOG_WARNING("Renderer::shutdown - worldMap...");
if (worldMap) {
worldMap->shutdown();
worldMap.reset();
}
LOG_WARNING("Renderer::shutdown - skySystem...");
if (skySystem) {
skySystem->shutdown();
skySystem.reset();
@ -772,34 +779,41 @@ void Renderer::shutdown() {
swimEffects.reset();
}
LOG_WARNING("Renderer::shutdown - characterRenderer...");
if (characterRenderer) {
characterRenderer->shutdown();
characterRenderer.reset();
}
LOG_WARNING("Renderer::shutdown - wmoRenderer...");
if (wmoRenderer) {
wmoRenderer->shutdown();
wmoRenderer.reset();
}
LOG_WARNING("Renderer::shutdown - m2Renderer...");
if (m2Renderer) {
m2Renderer->shutdown();
m2Renderer.reset();
}
LOG_WARNING("Renderer::shutdown - musicManager...");
if (musicManager) {
musicManager->shutdown();
musicManager.reset();
}
LOG_WARNING("Renderer::shutdown - footstepManager...");
if (footstepManager) {
footstepManager->shutdown();
footstepManager.reset();
}
LOG_WARNING("Renderer::shutdown - activitySoundManager...");
if (activitySoundManager) {
activitySoundManager->shutdown();
activitySoundManager.reset();
}
LOG_WARNING("Renderer::shutdown - AudioEngine...");
// Shutdown AudioEngine singleton
audio::AudioEngine::instance().shutdown();

View file

@ -129,17 +129,7 @@ TerrainManager::TerrainManager() {
}
TerrainManager::~TerrainManager() {
// Stop worker thread before cleanup (containers clean up via destructors)
if (workerRunning.load()) {
workerRunning.store(false);
queueCV.notify_all();
for (auto& t : workerThreads) {
if (t.joinable()) {
t.join();
}
}
workerThreads.clear();
}
stopWorkers();
}
bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) {
@ -276,6 +266,9 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
LOG_DEBUG("Preparing tile [", x, ",", y, "] (CPU work)");
// Early-exit check — worker should bail fast during shutdown
if (!workerRunning.load()) return nullptr;
// Load ADT file
std::string adtPath = getADTPath(coord);
auto adtData = assetManager->readFile(adtPath);
@ -294,6 +287,8 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
return nullptr;
}
if (!workerRunning.load()) return nullptr;
// WotLK split ADTs can store placements in *_obj0.adt.
// Merge object chunks so doodads/WMOs (including ground clutter) are available.
std::string objPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
@ -362,6 +357,8 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
return nullptr;
}
if (!workerRunning.load()) return nullptr;
auto pending = std::make_shared<PendingTile>();
pending->coord = coord;
pending->terrain = std::move(*terrainPtr);
@ -412,6 +409,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
// Pre-load M2 doodads (CPU: read files, parse models)
int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0;
for (const auto& placement : pending->terrain.doodadPlacements) {
if (!workerRunning.load()) return nullptr;
if (placement.nameId >= pending->terrain.doodadNames.size()) {
skippedNameId++;
continue;
@ -460,9 +458,12 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
ensureGroundEffectTablesLoaded();
generateGroundClutterPlacements(pending, preparedModelIds);
if (!workerRunning.load()) return nullptr;
// Pre-load WMOs (CPU: read files, parse models and groups)
if (!pending->terrain.wmoPlacements.empty()) {
for (const auto& placement : pending->terrain.wmoPlacements) {
if (!workerRunning.load()) return nullptr;
if (placement.nameId >= pending->terrain.wmoNames.size()) continue;
const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId];
@ -513,6 +514,7 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
);
// Pre-load WMO doodads (M2 models inside WMO)
if (!workerRunning.load()) return nullptr;
if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) {
glm::mat4 wmoMatrix(1.0f);
wmoMatrix = glm::translate(wmoMatrix, pos);
@ -636,6 +638,8 @@ std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
}
}
if (!workerRunning.load()) return nullptr;
// Pre-load terrain texture BLP data on background thread so finalizeTile
// doesn't block the main thread with file I/O.
for (const auto& texPath : pending->terrain.textures) {
@ -1068,6 +1072,28 @@ void TerrainManager::processAllReadyTiles() {
}
}
void TerrainManager::processOneReadyTile() {
// Move ready tiles into finalizing deque
{
std::lock_guard<std::mutex> lock(queueMutex);
while (!readyQueue.empty()) {
auto pending = readyQueue.front();
readyQueue.pop();
if (pending) {
FinalizingTile ft;
ft.pending = std::move(pending);
finalizingTiles_.push_back(std::move(ft));
}
}
}
// Finalize ONE tile completely, then return so caller can update the screen
if (!finalizingTiles_.empty()) {
auto& ft = finalizingTiles_.front();
while (!advanceFinalization(ft)) {}
finalizingTiles_.pop_front();
}
}
std::shared_ptr<PendingTile> TerrainManager::getCachedTile(const TileCoord& coord) {
std::lock_guard<std::mutex> lock(tileCacheMutex_);
auto it = tileCache_.find(coord);
@ -1237,6 +1263,29 @@ void TerrainManager::unloadTile(int x, int y) {
loadedTiles.erase(it);
}
void TerrainManager::stopWorkers() {
if (!workerRunning.load()) {
LOG_WARNING("stopWorkers: already stopped");
return;
}
LOG_WARNING("stopWorkers: signaling ", workerThreads.size(), " workers to stop...");
workerRunning.store(false);
queueCV.notify_all();
// Workers check workerRunning at each I/O point in prepareTile() and bail
// out quickly. Use plain join() which is safe with std::thread — no
// pthread_timedjoin_np (which silently joins the pthread but leaves the
// std::thread object thinking it's still joinable → std::terminate on dtor).
for (size_t i = 0; i < workerThreads.size(); i++) {
if (workerThreads[i].joinable()) {
LOG_WARNING("stopWorkers: joining worker ", i, "...");
workerThreads[i].join();
}
}
workerThreads.clear();
LOG_WARNING("stopWorkers: done");
}
void TerrainManager::unloadAll() {
// Signal worker threads to stop and wait briefly for them to finish.
// Workers may be mid-prepareTile (reading MPQ / parsing ADT) which can
@ -1245,29 +1294,8 @@ void TerrainManager::unloadAll() {
workerRunning.store(false);
queueCV.notify_all();
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
for (auto& t : workerThreads) {
if (!t.joinable()) continue;
// Try a timed wait via polling — std::thread has no timed join.
bool joined = false;
while (std::chrono::steady_clock::now() < deadline) {
// Check if thread finished by trying a native timed join
#ifdef __linux__
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_nsec += 50000000; // 50ms
if (ts.tv_nsec >= 1000000000) { ts.tv_sec++; ts.tv_nsec -= 1000000000; }
if (pthread_timedjoin_np(t.native_handle(), nullptr, &ts) == 0) {
joined = true;
break;
}
#else
std::this_thread::sleep_for(std::chrono::milliseconds(50));
#endif
}
if (!joined && t.joinable()) {
t.detach();
}
if (t.joinable()) t.join();
}
workerThreads.clear();
}

View file

@ -50,10 +50,12 @@ bool VkContext::initialize(SDL_Window* window) {
}
void VkContext::shutdown() {
LOG_WARNING("VkContext::shutdown - vkDeviceWaitIdle...");
if (device) {
vkDeviceWaitIdle(device);
}
LOG_WARNING("VkContext::shutdown - destroyImGuiResources...");
destroyImGuiResources();
// Destroy sync objects
@ -68,9 +70,16 @@ void VkContext::shutdown() {
if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; }
if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; }
LOG_WARNING("VkContext::shutdown - destroySwapchain...");
destroySwapchain();
if (allocator) { vmaDestroyAllocator(allocator); allocator = VK_NULL_HANDLE; }
// Skip vmaDestroyAllocator — it walks every allocation to free it, which
// takes many seconds with thousands of loaded textures/models. The driver
// reclaims all device memory when we destroy the device, and the OS reclaims
// everything on process exit. Skipping this makes shutdown instant.
allocator = VK_NULL_HANDLE;
LOG_WARNING("VkContext::shutdown - vkDestroyDevice...");
if (device) { vkDestroyDevice(device, nullptr); device = VK_NULL_HANDLE; }
if (surface) { vkDestroySurfaceKHR(instance, surface, nullptr); surface = VK_NULL_HANDLE; }
@ -83,7 +92,7 @@ void VkContext::shutdown() {
if (instance) { vkDestroyInstance(instance, nullptr); instance = VK_NULL_HANDLE; }
LOG_INFO("Vulkan context shutdown");
LOG_WARNING("Vulkan context shutdown complete");
}
bool VkContext::createInstance(SDL_Window* window) {