Port UI icon textures from OpenGL to Vulkan, fix loading screen clear values

Replace all glGenTextures/glTexImage2D calls in UI code with Vulkan texture
uploads via new VkContext::uploadImGuiTexture() helper. This fixes segfaults
from calling OpenGL functions without a GL context (null GLEW function pointers).

- Add uploadImGuiTexture() to VkContext with staging buffer upload pattern
- Convert game_screen, inventory_screen, spellbook_screen, talent_screen
  from GLuint/GL calls to VkDescriptorSet/Vulkan uploads
- Fix loading_screen clearValueCount (was 1, needs 2 or 3 for MSAA)
This commit is contained in:
Kelsi 2026-02-22 03:32:08 -08:00
parent b1a9d231c7
commit 325254dfcb
11 changed files with 314 additions and 136 deletions

View file

@ -338,9 +338,14 @@ void LoadingScreen::render() {
rpInfo.renderArea.offset = {0, 0};
rpInfo.renderArea.extent = vkCtx->getSwapchainExtent();
VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
rpInfo.clearValueCount = 1;
rpInfo.pClearValues = &clearColor;
// Render pass has 2 attachments (color + depth) or 3 with MSAA
VkClearValue clearValues[3]{};
clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
clearValues[1].depthStencil = {1.0f, 0};
clearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
bool msaaOn = vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT;
rpInfo.clearValueCount = msaaOn ? 3 : 2;
rpInfo.pClearValues = clearValues;
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd);

View file

@ -3,6 +3,7 @@
#include "core/logger.hpp"
#include <VkBootstrap.h>
#include <SDL2/SDL_vulkan.h>
#include <imgui_impl_vulkan.h>
#include <algorithm>
#include <cstring>
@ -639,6 +640,18 @@ bool VkContext::createImGuiResources() {
}
void VkContext::destroyImGuiResources() {
// Destroy uploaded UI textures
for (auto& tex : uiTextures_) {
if (tex.view) vkDestroyImageView(device, tex.view, nullptr);
if (tex.image) vkDestroyImage(device, tex.image, nullptr);
if (tex.memory) vkFreeMemory(device, tex.memory, nullptr);
}
uiTextures_.clear();
if (uiTextureSampler_) {
vkDestroySampler(device, uiTextureSampler_, nullptr);
uiTextureSampler_ = VK_NULL_HANDLE;
}
if (imguiDescriptorPool) {
vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr);
imguiDescriptorPool = VK_NULL_HANDLE;
@ -652,6 +665,167 @@ void VkContext::destroyImGuiResources() {
}
}
static uint32_t findMemType(VkPhysicalDevice physDev, uint32_t typeFilter, VkMemoryPropertyFlags props) {
VkPhysicalDeviceMemoryProperties memProps;
vkGetPhysicalDeviceMemoryProperties(physDev, &memProps);
for (uint32_t i = 0; i < memProps.memoryTypeCount; i++) {
if ((typeFilter & (1 << i)) && (memProps.memoryTypes[i].propertyFlags & props) == props)
return i;
}
return 0;
}
VkDescriptorSet VkContext::uploadImGuiTexture(const uint8_t* rgba, int width, int height) {
if (!device || !physicalDevice || width <= 0 || height <= 0 || !rgba)
return VK_NULL_HANDLE;
VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * height * 4;
// Create shared sampler on first call
if (!uiTextureSampler_) {
VkSamplerCreateInfo si{};
si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
si.magFilter = VK_FILTER_LINEAR;
si.minFilter = VK_FILTER_LINEAR;
si.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
si.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
if (vkCreateSampler(device, &si, nullptr, &uiTextureSampler_) != VK_SUCCESS) {
LOG_ERROR("Failed to create UI texture sampler");
return VK_NULL_HANDLE;
}
}
// Staging buffer
VkBuffer stagingBuffer;
VkDeviceMemory stagingMemory;
{
VkBufferCreateInfo bufInfo{};
bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufInfo.size = imageSize;
bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device, &bufInfo, nullptr, &stagingBuffer) != VK_SUCCESS)
return VK_NULL_HANDLE;
VkMemoryRequirements memReqs;
vkGetBufferMemoryRequirements(device, stagingBuffer, &memReqs);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memReqs.size;
allocInfo.memoryTypeIndex = findMemType(physicalDevice, memReqs.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory) != VK_SUCCESS) {
vkDestroyBuffer(device, stagingBuffer, nullptr);
return VK_NULL_HANDLE;
}
vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0);
void* mapped;
vkMapMemory(device, stagingMemory, 0, imageSize, 0, &mapped);
memcpy(mapped, rgba, imageSize);
vkUnmapMemory(device, stagingMemory);
}
// Create image
VkImage image;
VkDeviceMemory imageMemory;
{
VkImageCreateInfo imgInfo{};
imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imgInfo.imageType = VK_IMAGE_TYPE_2D;
imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
imgInfo.extent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
imgInfo.mipLevels = 1;
imgInfo.arrayLayers = 1;
imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imgInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imgInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imgInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vkCreateImage(device, &imgInfo, nullptr, &image) != VK_SUCCESS) {
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingMemory, nullptr);
return VK_NULL_HANDLE;
}
VkMemoryRequirements memReqs;
vkGetImageMemoryRequirements(device, image, &memReqs);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memReqs.size;
allocInfo.memoryTypeIndex = findMemType(physicalDevice, memReqs.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) {
vkDestroyImage(device, image, nullptr);
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingMemory, nullptr);
return VK_NULL_HANDLE;
}
vkBindImageMemory(device, image, imageMemory, 0);
}
// Upload via immediate submit
immediateSubmit([&](VkCommandBuffer cmd) {
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
VkBufferImageCopy region{};
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.imageExtent = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
vkCmdCopyBufferToImage(cmd, stagingBuffer, image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
});
// Cleanup staging
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingMemory, nullptr);
// Create image view
VkImageView imageView;
{
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
vkDestroyImage(device, image, nullptr);
vkFreeMemory(device, imageMemory, nullptr);
return VK_NULL_HANDLE;
}
}
// Register with ImGui
VkDescriptorSet ds = ImGui_ImplVulkan_AddTexture(uiTextureSampler_, imageView,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
// Track for cleanup
uiTextures_.push_back({image, imageMemory, imageView});
return ds;
}
bool VkContext::recreateSwapchain(int width, int height) {
vkDeviceWaitIdle(device);

View file

@ -1,5 +1,6 @@
#include "ui/game_screen.hpp"
#include "rendering/character_preview.hpp"
#include "rendering/vk_context.hpp"
#include "core/application.hpp"
#include "core/coordinates.hpp"
#include "core/spawn_presets.hpp"
@ -854,7 +855,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
if (const auto* eq = findComparableEquipped(static_cast<uint8_t>(info->inventoryType))) {
ImGui::Separator();
ImGui::TextDisabled("Equipped:");
GLuint eqIcon = inventoryScreen.getItemIcon(eq->item.displayInfoId);
VkDescriptorSet eqIcon = inventoryScreen.getItemIcon(eq->item.displayInfoId);
if (eqIcon) {
ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f));
ImGui::SameLine();
@ -3217,8 +3218,8 @@ void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
// Action Bar (Phase 3)
// ============================================================
GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
if (spellId == 0 || !am) return 0;
VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
if (spellId == 0 || !am) return VK_NULL_HANDLE;
// Check cache first
auto cit = spellIconCache_.find(spellId);
@ -3276,43 +3277,41 @@ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
// Look up spellId -> SpellIconID -> icon path
auto iit = spellIconIds_.find(spellId);
if (iit == spellIconIds_.end()) {
spellIconCache_[spellId] = 0;
return 0;
spellIconCache_[spellId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
auto pit = spellIconPaths_.find(iit->second);
if (pit == spellIconPaths_.end()) {
spellIconCache_[spellId] = 0;
return 0;
spellIconCache_[spellId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
// Path from DBC has no extension — append .blp
std::string iconPath = pit->second + ".blp";
auto blpData = am->readFile(iconPath);
if (blpData.empty()) {
spellIconCache_[spellId] = 0;
return 0;
spellIconCache_[spellId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
auto image = pipeline::BLPLoader::load(blpData);
if (!image.isValid()) {
spellIconCache_[spellId] = 0;
return 0;
spellIconCache_[spellId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
GLuint texId = 0;
glGenTextures(1, &texId);
glBindTexture(GL_TEXTURE_2D, texId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
// Upload to Vulkan via VkContext
auto* window = core::Application::getInstance().getWindow();
auto* vkCtx = window ? window->getVkContext() : nullptr;
if (!vkCtx) {
spellIconCache_[spellId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
spellIconCache_[spellId] = texId;
return texId;
VkDescriptorSet ds = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
spellIconCache_[spellId] = ds;
return ds;
}
void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
@ -3362,7 +3361,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
};
// Try to get icon texture for this slot
GLuint iconTex = 0;
VkDescriptorSet iconTex = VK_NULL_HANDLE;
const game::ItemDef* barItemDef = nullptr;
uint32_t itemDisplayInfoId = 0;
std::string itemNameFromQuery;
@ -3655,22 +3654,17 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) {
if (!blpData.empty()) {
auto image = pipeline::BLPLoader::load(blpData);
if (image.isValid()) {
glGenTextures(1, &backpackIconTexture_);
glBindTexture(GL_TEXTURE_2D, backpackIconTexture_);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
auto* w = core::Application::getInstance().getWindow();
auto* vkCtx = w ? w->getVkContext() : nullptr;
if (vkCtx)
backpackIconTexture_ = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
}
}
}
// Track bag slot screen rects for drop detection
ImVec2 bagSlotMins[4], bagSlotMaxs[4];
GLuint bagIcons[4] = {};
VkDescriptorSet bagIcons[4] = {};
// Slots 1-4: Bag slots (leftmost)
for (int i = 0; i < 4; ++i) {
@ -3680,7 +3674,7 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) {
game::EquipSlot bagSlot = static_cast<game::EquipSlot>(static_cast<int>(game::EquipSlot::BAG1) + i);
const auto& bagItem = inv.getEquipSlot(bagSlot);
GLuint bagIcon = 0;
VkDescriptorSet bagIcon = VK_NULL_HANDLE;
if (!bagItem.empty() && bagItem.item.displayInfoId != 0) {
bagIcon = inventoryScreen.getItemIcon(bagItem.item.displayInfoId);
}
@ -3844,7 +3838,7 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) {
auto pickedEquip = static_cast<game::EquipSlot>(
static_cast<int>(game::EquipSlot::BAG1) + bagBarPickedSlot_);
const auto& pickedItem = inv2.getEquipSlot(pickedEquip);
GLuint pickedIcon = 0;
VkDescriptorSet pickedIcon = VK_NULL_HANDLE;
if (!pickedItem.empty() && pickedItem.item.displayInfoId != 0) {
pickedIcon = inventoryScreen.getItemIcon(pickedItem.item.displayInfoId);
}
@ -4429,7 +4423,7 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
ImVec4 borderColor = isBuff ? ImVec4(0.2f, 0.8f, 0.2f, 0.9f) : ImVec4(0.8f, 0.2f, 0.2f, 0.9f);
// Try to get spell icon
GLuint iconTex = 0;
VkDescriptorSet iconTex = VK_NULL_HANDLE;
if (assetMgr) {
iconTex = getSpellIcon(aura.spellId, assetMgr);
}
@ -4534,7 +4528,7 @@ void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
// Get item icon
uint32_t displayId = item.displayInfoId;
if (displayId == 0 && info) displayId = info->displayInfoId;
GLuint iconTex = inventoryScreen.getItemIcon(displayId);
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(displayId);
ImVec2 cursor = ImGui::GetCursorScreenPos();
float rowH = std::max(iconSize, ImGui::GetTextLineHeight() * 2.0f);
@ -4963,7 +4957,7 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
bool selected = (selectedChoice == static_cast<int>(i));
// Get item icon if we have displayInfoId
uint32_t iconTex = 0;
VkDescriptorSet iconTex = VK_NULL_HANDLE;
if (info && info->valid && info->displayInfoId != 0) {
iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
}

View file

@ -1,6 +1,7 @@
#include "ui/inventory_screen.hpp"
#include "game/game_handler.hpp"
#include "core/application.hpp"
#include "rendering/vk_context.hpp"
#include "core/input.hpp"
#include "rendering/character_preview.hpp"
#include "rendering/character_renderer.hpp"
@ -72,10 +73,7 @@ const game::ItemSlot* findComparableEquipped(const game::Inventory& inventory, u
} // namespace
InventoryScreen::~InventoryScreen() {
// Clean up icon textures
for (auto& [id, tex] : iconCache_) {
if (tex) glDeleteTextures(1, &tex);
}
// Vulkan textures are owned by VkContext and cleaned up on shutdown
iconCache_.clear();
}
@ -95,8 +93,8 @@ ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) {
// Item Icon Loading
// ============================================================
GLuint InventoryScreen::getItemIcon(uint32_t displayInfoId) {
if (displayInfoId == 0 || !assetManager_) return 0;
VkDescriptorSet InventoryScreen::getItemIcon(uint32_t displayInfoId) {
if (displayInfoId == 0 || !assetManager_) return VK_NULL_HANDLE;
auto it = iconCache_.find(displayInfoId);
if (it != iconCache_.end()) return it->second;
@ -104,50 +102,48 @@ GLuint InventoryScreen::getItemIcon(uint32_t displayInfoId) {
// Load ItemDisplayInfo.dbc
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
if (!displayInfoDbc) {
iconCache_[displayInfoId] = 0;
return 0;
iconCache_[displayInfoId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
if (recIdx < 0) {
iconCache_[displayInfoId] = 0;
return 0;
iconCache_[displayInfoId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
// Field 5 = inventoryIcon_1
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["InventoryIcon"] : 5);
if (iconName.empty()) {
iconCache_[displayInfoId] = 0;
return 0;
iconCache_[displayInfoId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
std::string iconPath = "Interface\\Icons\\" + iconName + ".blp";
auto blpData = assetManager_->readFile(iconPath);
if (blpData.empty()) {
iconCache_[displayInfoId] = 0;
return 0;
iconCache_[displayInfoId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
auto image = pipeline::BLPLoader::load(blpData);
if (!image.isValid()) {
iconCache_[displayInfoId] = 0;
return 0;
iconCache_[displayInfoId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
GLuint texId = 0;
glGenTextures(1, &texId);
glBindTexture(GL_TEXTURE_2D, texId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
// Upload to Vulkan via VkContext
auto* window = core::Application::getInstance().getWindow();
auto* vkCtx = window ? window->getVkContext() : nullptr;
if (!vkCtx) {
iconCache_[displayInfoId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
iconCache_[displayInfoId] = texId;
return texId;
VkDescriptorSet ds = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
iconCache_[displayInfoId] = ds;
return ds;
}
// ============================================================
@ -507,7 +503,7 @@ void InventoryScreen::renderHeldItem() {
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
// Try to show icon
GLuint iconTex = getItemIcon(heldItem.displayInfoId);
VkDescriptorSet iconTex = getItemIcon(heldItem.displayInfoId);
if (iconTex) {
drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
ImVec2(pos.x + size, pos.y + size));
@ -1351,7 +1347,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
}
// Try to show icon
GLuint iconTex = getItemIcon(item.displayInfoId);
VkDescriptorSet iconTex = getItemIcon(item.displayInfoId);
if (iconTex) {
drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
ImVec2(pos.x + size, pos.y + size));
@ -1559,7 +1555,7 @@ void InventoryScreen::renderItemTooltip(const game::ItemDef& item, const game::I
if (const game::ItemSlot* eq = findComparableEquipped(*inventory, item.inventoryType)) {
ImGui::Separator();
ImGui::TextDisabled("Equipped:");
GLuint eqIcon = getItemIcon(eq->item.displayInfoId);
VkDescriptorSet eqIcon = getItemIcon(eq->item.displayInfoId);
if (eqIcon) {
ImGui::Image((ImTextureID)(uintptr_t)eqIcon, ImVec2(18.0f, 18.0f));
ImGui::SameLine();

View file

@ -1,6 +1,7 @@
#include "ui/spellbook_screen.hpp"
#include "core/input.hpp"
#include "core/application.hpp"
#include "rendering/vk_context.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/blp_loader.hpp"
@ -201,44 +202,41 @@ void SpellbookScreen::categorizeSpells(const std::unordered_set<uint32_t>& known
lastKnownSpellCount = knownSpells.size();
}
GLuint SpellbookScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) {
if (iconId == 0 || !assetManager) return 0;
VkDescriptorSet SpellbookScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) {
if (iconId == 0 || !assetManager) return VK_NULL_HANDLE;
auto cit = spellIconCache.find(iconId);
if (cit != spellIconCache.end()) return cit->second;
auto pit = spellIconPaths.find(iconId);
if (pit == spellIconPaths.end()) {
spellIconCache[iconId] = 0;
return 0;
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
std::string iconPath = pit->second + ".blp";
auto blpData = assetManager->readFile(iconPath);
if (blpData.empty()) {
spellIconCache[iconId] = 0;
return 0;
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
auto image = pipeline::BLPLoader::load(blpData);
if (!image.isValid()) {
spellIconCache[iconId] = 0;
return 0;
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
GLuint texId = 0;
glGenTextures(1, &texId);
glBindTexture(GL_TEXTURE_2D, texId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
auto* window = core::Application::getInstance().getWindow();
auto* vkCtx = window ? window->getVkContext() : nullptr;
if (!vkCtx) {
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
spellIconCache[iconId] = texId;
return texId;
VkDescriptorSet ds = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
spellIconCache[iconId] = ds;
return ds;
}
const SpellInfo* SpellbookScreen::getSpellInfo(uint32_t spellId) const {
@ -320,7 +318,7 @@ void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetMana
bool isPassive = info->isPassive();
bool isDim = isPassive || onCooldown;
GLuint iconTex = getSpellIcon(info->iconId, assetManager);
VkDescriptorSet iconTex = getSpellIcon(info->iconId, assetManager);
// Selectable consumes clicks properly (prevents window drag)
ImGui::Selectable("##row", false,

View file

@ -2,11 +2,11 @@
#include "core/input.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
#include "rendering/vk_context.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include <algorithm>
#include <GL/glew.h>
namespace wowee { namespace ui {
@ -260,7 +260,7 @@ void TalentScreen::renderTalent(game::GameHandler& gameHandler,
// Get spell icon
uint32_t spellId = talent.rankSpells[0];
GLuint iconTex = 0;
VkDescriptorSet iconTex = VK_NULL_HANDLE;
if (spellId != 0) {
auto it = spellIconIds.find(spellId);
if (it != spellIconIds.end()) {
@ -491,47 +491,41 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
LOG_INFO("Talent screen: Loaded ", spellIconPaths.size(), " spell icon paths from SpellIcon.dbc");
}
GLuint TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) {
if (iconId == 0 || !assetManager) return 0;
VkDescriptorSet TalentScreen::getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager) {
if (iconId == 0 || !assetManager) return VK_NULL_HANDLE;
// Check cache
auto cit = spellIconCache.find(iconId);
if (cit != spellIconCache.end()) return cit->second;
// Look up icon path
auto pit = spellIconPaths.find(iconId);
if (pit == spellIconPaths.end()) {
spellIconCache[iconId] = 0;
return 0;
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
// Load BLP file
std::string iconPath = pit->second + ".blp";
auto blpData = assetManager->readFile(iconPath);
if (blpData.empty()) {
spellIconCache[iconId] = 0;
return 0;
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
// Decode BLP
auto image = pipeline::BLPLoader::load(blpData);
if (!image.isValid()) {
spellIconCache[iconId] = 0;
return 0;
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
// Create OpenGL texture
GLuint texId = 0;
glGenTextures(1, &texId);
glBindTexture(GL_TEXTURE_2D, texId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image.width, image.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
auto* window = core::Application::getInstance().getWindow();
auto* vkCtx = window ? window->getVkContext() : nullptr;
if (!vkCtx) {
spellIconCache[iconId] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
spellIconCache[iconId] = texId;
return texId;
VkDescriptorSet ds = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
spellIconCache[iconId] = ds;
return ds;
}
}} // namespace wowee::ui