mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Merge commit '6bfa3dc402' into chore/split-mega-switch-to-map
This commit is contained in:
commit
fa2e8ad0fe
17 changed files with 418 additions and 97 deletions
|
|
@ -97,6 +97,7 @@ private:
|
||||||
void spawnPlayerCharacter();
|
void spawnPlayerCharacter();
|
||||||
std::string getPlayerModelPath() const;
|
std::string getPlayerModelPath() const;
|
||||||
static const char* mapIdToName(uint32_t mapId);
|
static const char* mapIdToName(uint32_t mapId);
|
||||||
|
static const char* mapDisplayName(uint32_t mapId);
|
||||||
void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z);
|
void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z);
|
||||||
void buildFactionHostilityMap(uint8_t playerRace);
|
void buildFactionHostilityMap(uint8_t playerRace);
|
||||||
pipeline::M2Model loadCreatureM2Sync(const std::string& m2Path);
|
pipeline::M2Model loadCreatureM2Sync(const std::string& m2Path);
|
||||||
|
|
|
||||||
|
|
@ -668,6 +668,7 @@ private:
|
||||||
VkCommandBuffer secondaryCmds_[NUM_SECONDARIES][MAX_FRAMES] = {};
|
VkCommandBuffer secondaryCmds_[NUM_SECONDARIES][MAX_FRAMES] = {};
|
||||||
|
|
||||||
bool parallelRecordingEnabled_ = false; // set true after pools/buffers created
|
bool parallelRecordingEnabled_ = false; // set true after pools/buffers created
|
||||||
|
bool endFrameInlineMode_ = false; // true when endFrame switched to INLINE render pass
|
||||||
bool createSecondaryCommandResources();
|
bool createSecondaryCommandResources();
|
||||||
void destroySecondaryCommandResources();
|
void destroySecondaryCommandResources();
|
||||||
VkCommandBuffer beginSecondary(uint32_t secondaryIndex);
|
VkCommandBuffer beginSecondary(uint32_t secondaryIndex);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -76,6 +78,7 @@ public:
|
||||||
bool isNvidiaGpu() const { return gpuVendorId_ == 0x10DE; }
|
bool isNvidiaGpu() const { return gpuVendorId_ == 0x10DE; }
|
||||||
VkQueue getGraphicsQueue() const { return graphicsQueue; }
|
VkQueue getGraphicsQueue() const { return graphicsQueue; }
|
||||||
uint32_t getGraphicsQueueFamily() const { return graphicsQueueFamily; }
|
uint32_t getGraphicsQueueFamily() const { return graphicsQueueFamily; }
|
||||||
|
bool hasDedicatedTransferQueue() const { return hasDedicatedTransfer_; }
|
||||||
VmaAllocator getAllocator() const { return allocator; }
|
VmaAllocator getAllocator() const { return allocator; }
|
||||||
VkSurfaceKHR getSurface() const { return surface; }
|
VkSurfaceKHR getSurface() const { return surface; }
|
||||||
VkPipelineCache getPipelineCache() const { return pipelineCache_; }
|
VkPipelineCache getPipelineCache() const { return pipelineCache_; }
|
||||||
|
|
@ -119,6 +122,18 @@ public:
|
||||||
VkImageView getDepthResolveImageView() const { return depthResolveImageView; }
|
VkImageView getDepthResolveImageView() const { return depthResolveImageView; }
|
||||||
VkImageView getDepthImageView() const { return depthImageView; }
|
VkImageView getDepthImageView() const { return depthImageView; }
|
||||||
|
|
||||||
|
// Sampler cache: returns a shared VkSampler matching the given create info.
|
||||||
|
// Callers must NOT destroy the returned sampler — it is owned by VkContext.
|
||||||
|
// Automatically clamps anisotropy if the device doesn't support it.
|
||||||
|
VkSampler getOrCreateSampler(const VkSamplerCreateInfo& info);
|
||||||
|
|
||||||
|
// Whether the physical device supports sampler anisotropy.
|
||||||
|
bool isSamplerAnisotropySupported() const { return samplerAnisotropySupported_; }
|
||||||
|
|
||||||
|
// Global sampler cache accessor (set during VkContext::initialize, cleared on shutdown).
|
||||||
|
// Used by VkTexture and other code that only has a VkDevice handle.
|
||||||
|
static VkContext* globalInstance() { return sInstance_; }
|
||||||
|
|
||||||
// UI texture upload: creates a Vulkan texture from RGBA data and returns
|
// UI texture upload: creates a Vulkan texture from RGBA data and returns
|
||||||
// a VkDescriptorSet suitable for use as ImTextureID.
|
// a VkDescriptorSet suitable for use as ImTextureID.
|
||||||
// The caller does NOT need to free the result — resources are tracked and
|
// The caller does NOT need to free the result — resources are tracked and
|
||||||
|
|
@ -161,6 +176,12 @@ private:
|
||||||
uint32_t graphicsQueueFamily = 0;
|
uint32_t graphicsQueueFamily = 0;
|
||||||
uint32_t presentQueueFamily = 0;
|
uint32_t presentQueueFamily = 0;
|
||||||
|
|
||||||
|
// Dedicated transfer queue (second queue from same graphics family)
|
||||||
|
VkQueue transferQueue_ = VK_NULL_HANDLE;
|
||||||
|
VkCommandPool transferCommandPool_ = VK_NULL_HANDLE;
|
||||||
|
bool hasDedicatedTransfer_ = false;
|
||||||
|
uint32_t graphicsQueueFamilyQueueCount_ = 1; // queried in selectPhysicalDevice
|
||||||
|
|
||||||
// Swapchain
|
// Swapchain
|
||||||
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
|
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
|
||||||
VkFormat swapchainFormat = VK_FORMAT_UNDEFINED;
|
VkFormat swapchainFormat = VK_FORMAT_UNDEFINED;
|
||||||
|
|
@ -239,6 +260,13 @@ private:
|
||||||
};
|
};
|
||||||
std::vector<UiTexture> uiTextures_;
|
std::vector<UiTexture> uiTextures_;
|
||||||
|
|
||||||
|
// Sampler cache — deduplicates VkSamplers by configuration hash.
|
||||||
|
std::mutex samplerCacheMutex_;
|
||||||
|
std::unordered_map<uint64_t, VkSampler> samplerCache_;
|
||||||
|
bool samplerAnisotropySupported_ = false;
|
||||||
|
|
||||||
|
static VkContext* sInstance_;
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
bool enableValidation = true;
|
bool enableValidation = true;
|
||||||
#else
|
#else
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ private:
|
||||||
bool hasDepth_ = false;
|
bool hasDepth_ = false;
|
||||||
VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT;
|
||||||
VkSampler sampler_ = VK_NULL_HANDLE;
|
VkSampler sampler_ = VK_NULL_HANDLE;
|
||||||
|
bool ownsSampler_ = true;
|
||||||
VkRenderPass renderPass_ = VK_NULL_HANDLE;
|
VkRenderPass renderPass_ = VK_NULL_HANDLE;
|
||||||
VkFramebuffer framebuffer_ = VK_NULL_HANDLE;
|
VkFramebuffer framebuffer_ = VK_NULL_HANDLE;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ private:
|
||||||
AllocatedImage image_{};
|
AllocatedImage image_{};
|
||||||
VkSampler sampler_ = VK_NULL_HANDLE;
|
VkSampler sampler_ = VK_NULL_HANDLE;
|
||||||
uint32_t mipLevels_ = 1;
|
uint32_t mipLevels_ = 1;
|
||||||
|
bool ownsSampler_ = true; // false when sampler comes from VkContext cache
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -52,18 +52,14 @@ bool ActivitySoundManager::initialize(pipeline::AssetManager* assets) {
|
||||||
preloadLandingSet(FootstepSurface::SNOW, "Snow");
|
preloadLandingSet(FootstepSurface::SNOW, "Snow");
|
||||||
|
|
||||||
preloadCandidates(meleeSwingClips, {
|
preloadCandidates(meleeSwingClips, {
|
||||||
"Sound\\Item\\Weapons\\Sword\\SwordSwing1.wav",
|
"Sound\\Item\\Weapons\\WeaponSwings\\mWooshMedium1.wav",
|
||||||
"Sound\\Item\\Weapons\\Sword\\SwordSwing2.wav",
|
"Sound\\Item\\Weapons\\WeaponSwings\\mWooshMedium2.wav",
|
||||||
"Sound\\Item\\Weapons\\Sword\\SwordSwing3.wav",
|
"Sound\\Item\\Weapons\\WeaponSwings\\mWooshMedium3.wav",
|
||||||
"Sound\\Item\\Weapons\\Sword\\SwordHit1.wav",
|
"Sound\\Item\\Weapons\\WeaponSwings\\mWooshLarge1.wav",
|
||||||
"Sound\\Item\\Weapons\\Sword\\SwordHit2.wav",
|
"Sound\\Item\\Weapons\\WeaponSwings\\mWooshLarge2.wav",
|
||||||
"Sound\\Item\\Weapons\\Sword\\SwordHit3.wav",
|
"Sound\\Item\\Weapons\\WeaponSwings\\mWooshLarge3.wav",
|
||||||
"Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing1.wav",
|
"Sound\\Item\\Weapons\\MissSwings\\MissWhoosh1Handed.wav",
|
||||||
"Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing2.wav",
|
"Sound\\Item\\Weapons\\MissSwings\\MissWhoosh2Handed.wav"
|
||||||
"Sound\\Item\\Weapons\\OneHanded\\Sword\\SwordSwing3.wav",
|
|
||||||
"Sound\\Item\\Weapons\\Melee\\MeleeSwing1.wav",
|
|
||||||
"Sound\\Item\\Weapons\\Melee\\MeleeSwing2.wav",
|
|
||||||
"Sound\\Item\\Weapons\\Melee\\MeleeSwing3.wav"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,17 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
const char* Application::mapDisplayName(uint32_t mapId) {
|
||||||
|
// Friendly display names for the loading screen
|
||||||
|
switch (mapId) {
|
||||||
|
case 0: return "Eastern Kingdoms";
|
||||||
|
case 1: return "Kalimdor";
|
||||||
|
case 530: return "Outland";
|
||||||
|
case 571: return "Northrend";
|
||||||
|
default: return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const char* Application::mapIdToName(uint32_t mapId) {
|
const char* Application::mapIdToName(uint32_t mapId) {
|
||||||
// Fallback when Map.dbc is unavailable. Names must match WDT directory names
|
// Fallback when Map.dbc is unavailable. Names must match WDT directory names
|
||||||
// (case-insensitive — AssetManager lowercases all paths).
|
// (case-insensitive — AssetManager lowercases all paths).
|
||||||
|
|
@ -4468,13 +4479,18 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
window->swapBuffers();
|
window->swapBuffers();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set zone name on loading screen from Map.dbc
|
// Set zone name on loading screen — prefer friendly display name, then DBC
|
||||||
if (gameHandler) {
|
{
|
||||||
std::string mapDisplayName = gameHandler->getMapName(mapId);
|
const char* friendly = mapDisplayName(mapId);
|
||||||
if (!mapDisplayName.empty())
|
if (friendly) {
|
||||||
loadingScreen.setZoneName(mapDisplayName);
|
loadingScreen.setZoneName(friendly);
|
||||||
else
|
} else if (gameHandler) {
|
||||||
loadingScreen.setZoneName("Loading...");
|
std::string dbcName = gameHandler->getMapName(mapId);
|
||||||
|
if (!dbcName.empty())
|
||||||
|
loadingScreen.setZoneName(dbcName);
|
||||||
|
else
|
||||||
|
loadingScreen.setZoneName("Loading...");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showProgress("Entering world...", 0.0f);
|
showProgress("Entering world...", 0.0f);
|
||||||
|
|
@ -5295,6 +5311,14 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
|
|
||||||
showProgress("Entering world...", 1.0f);
|
showProgress("Entering world...", 1.0f);
|
||||||
|
|
||||||
|
// Ensure all GPU resources (textures, buffers, pipelines) created during
|
||||||
|
// world load are fully flushed before the first render frame. Without this,
|
||||||
|
// vkCmdBeginRenderPass can crash on NVIDIA 590.x when resources from async
|
||||||
|
// uploads haven't completed their queue operations.
|
||||||
|
if (renderer && renderer->getVkContext()) {
|
||||||
|
vkDeviceWaitIdle(renderer->getVkContext()->getDevice());
|
||||||
|
}
|
||||||
|
|
||||||
if (loadingScreenOk) {
|
if (loadingScreenOk) {
|
||||||
loadingScreen.shutdown();
|
loadingScreen.shutdown();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18844,8 +18844,8 @@ void GameHandler::handleSpellStart(network::Packet& packet) {
|
||||||
castTimeRemaining = castTimeTotal;
|
castTimeRemaining = castTimeTotal;
|
||||||
if (addonEventCallback_) addonEventCallback_("CURRENT_SPELL_CAST_CHANGED", {});
|
if (addonEventCallback_) addonEventCallback_("CURRENT_SPELL_CAST_CHANGED", {});
|
||||||
|
|
||||||
// Play precast (channeling) sound with correct magic school
|
// Play precast sound — skip profession/tradeskill spells (they use crafting
|
||||||
// Skip sound for profession/tradeskill spells (crafting should be silent)
|
// animations/sounds, not magic spell audio).
|
||||||
if (!isProfessionSpell(data.spellId)) {
|
if (!isProfessionSpell(data.spellId)) {
|
||||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||||
if (auto* ssm = renderer->getSpellSoundManager()) {
|
if (auto* ssm = renderer->getSpellSoundManager()) {
|
||||||
|
|
@ -18891,8 +18891,7 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
|
||||||
|
|
||||||
// Cast completed
|
// Cast completed
|
||||||
if (data.casterUnit == playerGuid) {
|
if (data.casterUnit == playerGuid) {
|
||||||
// Play cast-complete sound with correct magic school
|
// Play cast-complete sound — skip profession spells (no magic sound for crafting)
|
||||||
// Skip sound for profession/tradeskill spells (crafting should be silent)
|
|
||||||
if (!isProfessionSpell(data.spellId)) {
|
if (!isProfessionSpell(data.spellId)) {
|
||||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||||
if (auto* ssm = renderer->getSpellSoundManager()) {
|
if (auto* ssm = renderer->getSpellSoundManager()) {
|
||||||
|
|
@ -18908,11 +18907,12 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
|
||||||
|
|
||||||
// Instant melee abilities → trigger attack animation
|
// Instant melee abilities → trigger attack animation
|
||||||
// Detect via physical school mask (1 = Physical) from the spell DBC cache.
|
// Detect via physical school mask (1 = Physical) from the spell DBC cache.
|
||||||
|
// Skip profession spells — crafting should not swing weapons.
|
||||||
// This covers warrior, rogue, DK, paladin, feral druid, and hunter melee
|
// This covers warrior, rogue, DK, paladin, feral druid, and hunter melee
|
||||||
// abilities generically instead of maintaining a brittle per-spell-ID list.
|
// abilities generically instead of maintaining a brittle per-spell-ID list.
|
||||||
uint32_t sid = data.spellId;
|
uint32_t sid = data.spellId;
|
||||||
bool isMeleeAbility = false;
|
bool isMeleeAbility = false;
|
||||||
{
|
if (!isProfessionSpell(sid)) {
|
||||||
loadSpellNameCache();
|
loadSpellNameCache();
|
||||||
auto cacheIt = spellNameCache_.find(sid);
|
auto cacheIt = spellNameCache_.find(sid);
|
||||||
if (cacheIt != spellNameCache_.end() && cacheIt->second.schoolMask == 1) {
|
if (cacheIt != spellNameCache_.end() && cacheIt->second.schoolMask == 1) {
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,7 @@ void LoadingScreen::shutdown() {
|
||||||
// ImGui manages descriptor set lifetime
|
// ImGui manages descriptor set lifetime
|
||||||
bgDescriptorSet = VK_NULL_HANDLE;
|
bgDescriptorSet = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (bgSampler) {
|
bgSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
vkDestroySampler(device, bgSampler, nullptr);
|
|
||||||
bgSampler = VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
if (bgImageView) {
|
if (bgImageView) {
|
||||||
vkDestroyImageView(device, bgImageView, nullptr);
|
vkDestroyImageView(device, bgImageView, nullptr);
|
||||||
bgImageView = VK_NULL_HANDLE;
|
bgImageView = VK_NULL_HANDLE;
|
||||||
|
|
@ -94,7 +91,7 @@ bool LoadingScreen::loadImage(const std::string& path) {
|
||||||
if (bgImage) {
|
if (bgImage) {
|
||||||
VkDevice device = vkCtx->getDevice();
|
VkDevice device = vkCtx->getDevice();
|
||||||
vkDeviceWaitIdle(device);
|
vkDeviceWaitIdle(device);
|
||||||
if (bgSampler) { vkDestroySampler(device, bgSampler, nullptr); bgSampler = VK_NULL_HANDLE; }
|
bgSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; }
|
if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; }
|
||||||
if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; }
|
if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; }
|
||||||
if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; }
|
if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; }
|
||||||
|
|
@ -230,7 +227,7 @@ bool LoadingScreen::loadImage(const std::string& path) {
|
||||||
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
vkCreateSampler(device, &samplerInfo, nullptr, &bgSampler);
|
bgSampler = vkCtx->getOrCreateSampler(samplerInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register with ImGui as a texture
|
// Register with ImGui as a texture
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,8 @@ bool Renderer::createPerFrameResources() {
|
||||||
sampCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
sampCI.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||||
sampCI.compareEnable = VK_TRUE;
|
sampCI.compareEnable = VK_TRUE;
|
||||||
sampCI.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
|
sampCI.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
|
||||||
if (vkCreateSampler(device, &sampCI, nullptr, &shadowSampler) != VK_SUCCESS) {
|
shadowSampler = vkCtx->getOrCreateSampler(sampCI);
|
||||||
|
if (shadowSampler == VK_NULL_HANDLE) {
|
||||||
LOG_ERROR("Failed to create shadow sampler");
|
LOG_ERROR("Failed to create shadow sampler");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -597,7 +598,7 @@ void Renderer::destroyPerFrameResources() {
|
||||||
shadowDepthLayout_[i] = VK_IMAGE_LAYOUT_UNDEFINED;
|
shadowDepthLayout_[i] = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
}
|
}
|
||||||
if (shadowRenderPass) { vkDestroyRenderPass(device, shadowRenderPass, nullptr); shadowRenderPass = VK_NULL_HANDLE; }
|
if (shadowRenderPass) { vkDestroyRenderPass(device, shadowRenderPass, nullptr); shadowRenderPass = VK_NULL_HANDLE; }
|
||||||
if (shadowSampler) { vkDestroySampler(device, shadowSampler, nullptr); shadowSampler = VK_NULL_HANDLE; }
|
shadowSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::updatePerFrameUBO() {
|
void Renderer::updatePerFrameUBO() {
|
||||||
|
|
@ -1214,6 +1215,11 @@ void Renderer::beginFrame() {
|
||||||
void Renderer::endFrame() {
|
void Renderer::endFrame() {
|
||||||
if (!vkCtx || currentCmd == VK_NULL_HANDLE) return;
|
if (!vkCtx || currentCmd == VK_NULL_HANDLE) return;
|
||||||
|
|
||||||
|
// Track whether a post-processing path switched to an INLINE render pass.
|
||||||
|
// beginFrame() may have started the scene pass with SECONDARY_COMMAND_BUFFERS;
|
||||||
|
// post-proc paths end it and begin a new INLINE pass for the swapchain output.
|
||||||
|
endFrameInlineMode_ = false;
|
||||||
|
|
||||||
if (fsr2_.enabled && fsr2_.sceneFramebuffer) {
|
if (fsr2_.enabled && fsr2_.sceneFramebuffer) {
|
||||||
// End the off-screen scene render pass
|
// End the off-screen scene render pass
|
||||||
vkCmdEndRenderPass(currentCmd);
|
vkCmdEndRenderPass(currentCmd);
|
||||||
|
|
@ -1296,7 +1302,7 @@ void Renderer::endFrame() {
|
||||||
rpInfo.clearValueCount = msaaOn ? (vkCtx->getDepthResolveImageView() ? 4u : 3u) : 2u;
|
rpInfo.clearValueCount = msaaOn ? (vkCtx->getDepthResolveImageView() ? 4u : 3u) : 2u;
|
||||||
rpInfo.pClearValues = clearValues;
|
rpInfo.pClearValues = clearValues;
|
||||||
|
|
||||||
vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
|
endFrameInlineMode_ = true; vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||||
|
|
||||||
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
VkExtent2D ext = vkCtx->getSwapchainExtent();
|
||||||
VkViewport vp{};
|
VkViewport vp{};
|
||||||
|
|
@ -1433,18 +1439,22 @@ void Renderer::endFrame() {
|
||||||
renderFSRUpscale();
|
renderFSRUpscale();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImGui rendering — must respect subpass contents mode
|
// ImGui rendering — must respect the subpass contents mode of the
|
||||||
// Parallel recording only applies when no post-process pass is active.
|
// CURRENT render pass. Post-processing paths (FSR/FXAA) end the scene
|
||||||
if (!fsr_.enabled && !fsr2_.enabled && !fxaa_.enabled && parallelRecordingEnabled_) {
|
// pass and begin a new INLINE pass; if none ran, we're still inside the
|
||||||
// Scene pass was begun with VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS,
|
// scene pass which may be SECONDARY_COMMAND_BUFFERS when parallel recording
|
||||||
// so ImGui must be recorded into a secondary command buffer.
|
// is active. Track this via endFrameInlineMode_ (set true by any post-proc
|
||||||
|
// path that started an INLINE render pass).
|
||||||
|
if (parallelRecordingEnabled_ && !endFrameInlineMode_) {
|
||||||
|
// Still in the scene pass with SECONDARY_COMMAND_BUFFERS — record
|
||||||
|
// ImGui into a secondary command buffer.
|
||||||
VkCommandBuffer imguiCmd = beginSecondary(SEC_IMGUI);
|
VkCommandBuffer imguiCmd = beginSecondary(SEC_IMGUI);
|
||||||
setSecondaryViewportScissor(imguiCmd);
|
setSecondaryViewportScissor(imguiCmd);
|
||||||
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), imguiCmd);
|
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), imguiCmd);
|
||||||
vkEndCommandBuffer(imguiCmd);
|
vkEndCommandBuffer(imguiCmd);
|
||||||
vkCmdExecuteCommands(currentCmd, 1, &imguiCmd);
|
vkCmdExecuteCommands(currentCmd, 1, &imguiCmd);
|
||||||
} else {
|
} else {
|
||||||
// FSR swapchain pass uses INLINE mode; non-parallel also uses INLINE.
|
// INLINE render pass (post-process pass or non-parallel mode).
|
||||||
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), currentCmd);
|
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), currentCmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4057,7 +4067,8 @@ bool Renderer::initFSRResources() {
|
||||||
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
||||||
if (vkCreateSampler(device, &samplerInfo, nullptr, &fsr_.sceneSampler) != VK_SUCCESS) {
|
fsr_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo);
|
||||||
|
if (fsr_.sceneSampler == VK_NULL_HANDLE) {
|
||||||
LOG_ERROR("FSR: failed to create sampler");
|
LOG_ERROR("FSR: failed to create sampler");
|
||||||
destroyFSRResources();
|
destroyFSRResources();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -4171,7 +4182,7 @@ void Renderer::destroyFSRResources() {
|
||||||
if (fsr_.descPool) { vkDestroyDescriptorPool(device, fsr_.descPool, nullptr); fsr_.descPool = VK_NULL_HANDLE; fsr_.descSet = VK_NULL_HANDLE; }
|
if (fsr_.descPool) { vkDestroyDescriptorPool(device, fsr_.descPool, nullptr); fsr_.descPool = VK_NULL_HANDLE; fsr_.descSet = VK_NULL_HANDLE; }
|
||||||
if (fsr_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fsr_.descSetLayout, nullptr); fsr_.descSetLayout = VK_NULL_HANDLE; }
|
if (fsr_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fsr_.descSetLayout, nullptr); fsr_.descSetLayout = VK_NULL_HANDLE; }
|
||||||
if (fsr_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr_.sceneFramebuffer, nullptr); fsr_.sceneFramebuffer = VK_NULL_HANDLE; }
|
if (fsr_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr_.sceneFramebuffer, nullptr); fsr_.sceneFramebuffer = VK_NULL_HANDLE; }
|
||||||
if (fsr_.sceneSampler) { vkDestroySampler(device, fsr_.sceneSampler, nullptr); fsr_.sceneSampler = VK_NULL_HANDLE; }
|
fsr_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
destroyImage(device, alloc, fsr_.sceneDepthResolve);
|
destroyImage(device, alloc, fsr_.sceneDepthResolve);
|
||||||
destroyImage(device, alloc, fsr_.sceneMsaaColor);
|
destroyImage(device, alloc, fsr_.sceneMsaaColor);
|
||||||
destroyImage(device, alloc, fsr_.sceneDepth);
|
destroyImage(device, alloc, fsr_.sceneDepth);
|
||||||
|
|
@ -4350,11 +4361,11 @@ bool Renderer::initFSR2Resources() {
|
||||||
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
vkCreateSampler(device, &samplerInfo, nullptr, &fsr2_.linearSampler);
|
fsr2_.linearSampler = vkCtx->getOrCreateSampler(samplerInfo);
|
||||||
|
|
||||||
samplerInfo.minFilter = VK_FILTER_NEAREST;
|
samplerInfo.minFilter = VK_FILTER_NEAREST;
|
||||||
samplerInfo.magFilter = VK_FILTER_NEAREST;
|
samplerInfo.magFilter = VK_FILTER_NEAREST;
|
||||||
vkCreateSampler(device, &samplerInfo, nullptr, &fsr2_.nearestSampler);
|
fsr2_.nearestSampler = vkCtx->getOrCreateSampler(samplerInfo);
|
||||||
|
|
||||||
#if WOWEE_HAS_AMD_FSR2
|
#if WOWEE_HAS_AMD_FSR2
|
||||||
// Initialize AMD FSR2 context; fall back to internal path on any failure.
|
// Initialize AMD FSR2 context; fall back to internal path on any failure.
|
||||||
|
|
@ -4753,8 +4764,8 @@ void Renderer::destroyFSR2Resources() {
|
||||||
if (fsr2_.motionVecDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.motionVecDescSetLayout, nullptr); fsr2_.motionVecDescSetLayout = VK_NULL_HANDLE; }
|
if (fsr2_.motionVecDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.motionVecDescSetLayout, nullptr); fsr2_.motionVecDescSetLayout = VK_NULL_HANDLE; }
|
||||||
|
|
||||||
if (fsr2_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr2_.sceneFramebuffer, nullptr); fsr2_.sceneFramebuffer = VK_NULL_HANDLE; }
|
if (fsr2_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr2_.sceneFramebuffer, nullptr); fsr2_.sceneFramebuffer = VK_NULL_HANDLE; }
|
||||||
if (fsr2_.linearSampler) { vkDestroySampler(device, fsr2_.linearSampler, nullptr); fsr2_.linearSampler = VK_NULL_HANDLE; }
|
fsr2_.linearSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
if (fsr2_.nearestSampler) { vkDestroySampler(device, fsr2_.nearestSampler, nullptr); fsr2_.nearestSampler = VK_NULL_HANDLE; }
|
fsr2_.nearestSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
|
|
||||||
destroyImage(device, alloc, fsr2_.motionVectors);
|
destroyImage(device, alloc, fsr2_.motionVectors);
|
||||||
for (int i = 0; i < 2; i++) destroyImage(device, alloc, fsr2_.history[i]);
|
for (int i = 0; i < 2; i++) destroyImage(device, alloc, fsr2_.history[i]);
|
||||||
|
|
@ -5273,7 +5284,8 @@ bool Renderer::initFXAAResources() {
|
||||||
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
||||||
if (vkCreateSampler(device, &samplerInfo, nullptr, &fxaa_.sceneSampler) != VK_SUCCESS) {
|
fxaa_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo);
|
||||||
|
if (fxaa_.sceneSampler == VK_NULL_HANDLE) {
|
||||||
LOG_ERROR("FXAA: failed to create sampler");
|
LOG_ERROR("FXAA: failed to create sampler");
|
||||||
destroyFXAAResources();
|
destroyFXAAResources();
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -5383,7 +5395,7 @@ void Renderer::destroyFXAAResources() {
|
||||||
if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; }
|
if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; }
|
||||||
if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; }
|
if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; }
|
||||||
if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; }
|
if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; }
|
||||||
if (fxaa_.sceneSampler) { vkDestroySampler(device, fxaa_.sceneSampler, nullptr); fxaa_.sceneSampler = VK_NULL_HANDLE; }
|
fxaa_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
destroyImage(device, alloc, fxaa_.sceneDepthResolve);
|
destroyImage(device, alloc, fxaa_.sceneDepthResolve);
|
||||||
destroyImage(device, alloc, fxaa_.sceneMsaaColor);
|
destroyImage(device, alloc, fxaa_.sceneMsaaColor);
|
||||||
destroyImage(device, alloc, fxaa_.sceneDepth);
|
destroyImage(device, alloc, fxaa_.sceneDepth);
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ void TerrainManager::update(const Camera& camera, float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always process ready tiles each frame (GPU uploads from background thread)
|
// Always process ready tiles each frame (GPU uploads from background thread)
|
||||||
// Time budget prevents frame spikes from heavy tiles
|
// Time-budgeted internally to prevent frame spikes.
|
||||||
processReadyTiles();
|
processReadyTiles();
|
||||||
|
|
||||||
timeSinceLastUpdate += deltaTime;
|
timeSinceLastUpdate += deltaTime;
|
||||||
|
|
@ -1223,18 +1223,25 @@ void TerrainManager::processReadyTiles() {
|
||||||
// Async upload batch: record GPU copies into a command buffer, submit with
|
// Async upload batch: record GPU copies into a command buffer, submit with
|
||||||
// a fence, but DON'T wait. The fence is polled on subsequent frames.
|
// a fence, but DON'T wait. The fence is polled on subsequent frames.
|
||||||
// This eliminates the main-thread stall from vkWaitForFences entirely.
|
// This eliminates the main-thread stall from vkWaitForFences entirely.
|
||||||
const int maxSteps = taxiStreamingMode_ ? 4 : 1;
|
//
|
||||||
int steps = 0;
|
// Time-budgeted: yield after 8ms to prevent main-loop stalls. Each
|
||||||
|
// advanceFinalization step is designed to be small, but texture uploads
|
||||||
|
// and M2 model loads can occasionally spike. The budget ensures we
|
||||||
|
// spread heavy tiles across multiple frames instead of blocking.
|
||||||
|
const auto budgetStart = std::chrono::steady_clock::now();
|
||||||
|
const float budgetMs = taxiStreamingMode_ ? 16.0f : 8.0f;
|
||||||
|
|
||||||
if (vkCtx) vkCtx->beginUploadBatch();
|
if (vkCtx) vkCtx->beginUploadBatch();
|
||||||
|
|
||||||
while (!finalizingTiles_.empty() && steps < maxSteps) {
|
while (!finalizingTiles_.empty()) {
|
||||||
auto& ft = finalizingTiles_.front();
|
auto& ft = finalizingTiles_.front();
|
||||||
bool done = advanceFinalization(ft);
|
bool done = advanceFinalization(ft);
|
||||||
if (done) {
|
if (done) {
|
||||||
finalizingTiles_.pop_front();
|
finalizingTiles_.pop_front();
|
||||||
}
|
}
|
||||||
steps++;
|
float elapsed = std::chrono::duration<float, std::milli>(
|
||||||
|
std::chrono::steady_clock::now() - budgetStart).count();
|
||||||
|
if (elapsed >= budgetMs) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vkCtx) vkCtx->endUploadBatch(); // Async — submits but doesn't wait
|
if (vkCtx) vkCtx->endUploadBatch(); // Async — submits but doesn't wait
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,44 @@
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
|
VkContext* VkContext::sInstance_ = nullptr;
|
||||||
|
|
||||||
|
// Hash a VkSamplerCreateInfo into a 64-bit key for the sampler cache.
|
||||||
|
static uint64_t hashSamplerCreateInfo(const VkSamplerCreateInfo& s) {
|
||||||
|
// Pack the relevant fields into a deterministic hash.
|
||||||
|
// FNV-1a 64-bit on the raw config values.
|
||||||
|
uint64_t h = 14695981039346656037ULL;
|
||||||
|
auto mix = [&](uint64_t v) {
|
||||||
|
h ^= v;
|
||||||
|
h *= 1099511628211ULL;
|
||||||
|
};
|
||||||
|
mix(static_cast<uint64_t>(s.minFilter));
|
||||||
|
mix(static_cast<uint64_t>(s.magFilter));
|
||||||
|
mix(static_cast<uint64_t>(s.mipmapMode));
|
||||||
|
mix(static_cast<uint64_t>(s.addressModeU));
|
||||||
|
mix(static_cast<uint64_t>(s.addressModeV));
|
||||||
|
mix(static_cast<uint64_t>(s.addressModeW));
|
||||||
|
mix(static_cast<uint64_t>(s.anisotropyEnable));
|
||||||
|
// Bit-cast floats to uint32_t for hashing
|
||||||
|
uint32_t aniso;
|
||||||
|
std::memcpy(&aniso, &s.maxAnisotropy, sizeof(aniso));
|
||||||
|
mix(static_cast<uint64_t>(aniso));
|
||||||
|
uint32_t maxLodBits;
|
||||||
|
std::memcpy(&maxLodBits, &s.maxLod, sizeof(maxLodBits));
|
||||||
|
mix(static_cast<uint64_t>(maxLodBits));
|
||||||
|
uint32_t minLodBits;
|
||||||
|
std::memcpy(&minLodBits, &s.minLod, sizeof(minLodBits));
|
||||||
|
mix(static_cast<uint64_t>(minLodBits));
|
||||||
|
mix(static_cast<uint64_t>(s.compareEnable));
|
||||||
|
mix(static_cast<uint64_t>(s.compareOp));
|
||||||
|
mix(static_cast<uint64_t>(s.borderColor));
|
||||||
|
uint32_t biasBits;
|
||||||
|
std::memcpy(&biasBits, &s.mipLodBias, sizeof(biasBits));
|
||||||
|
mix(static_cast<uint64_t>(biasBits));
|
||||||
|
mix(static_cast<uint64_t>(s.unnormalizedCoordinates));
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
|
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
|
||||||
VkDebugUtilsMessageSeverityFlagBitsEXT severity,
|
VkDebugUtilsMessageSeverityFlagBitsEXT severity,
|
||||||
[[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT type,
|
[[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT type,
|
||||||
|
|
@ -52,6 +90,14 @@ bool VkContext::initialize(SDL_Window* window) {
|
||||||
if (!createSyncObjects()) return false;
|
if (!createSyncObjects()) return false;
|
||||||
if (!createImGuiResources()) return false;
|
if (!createImGuiResources()) return false;
|
||||||
|
|
||||||
|
// Query anisotropy support from the physical device.
|
||||||
|
VkPhysicalDeviceFeatures supportedFeatures{};
|
||||||
|
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
|
||||||
|
samplerAnisotropySupported_ = (supportedFeatures.samplerAnisotropy == VK_TRUE);
|
||||||
|
LOG_INFO("Sampler anisotropy supported: ", samplerAnisotropySupported_ ? "YES" : "NO");
|
||||||
|
|
||||||
|
sInstance_ = this;
|
||||||
|
|
||||||
LOG_INFO("Vulkan context initialized successfully");
|
LOG_INFO("Vulkan context initialized successfully");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +135,7 @@ void VkContext::shutdown() {
|
||||||
|
|
||||||
if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; }
|
if (immFence) { vkDestroyFence(device, immFence, nullptr); immFence = VK_NULL_HANDLE; }
|
||||||
if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; }
|
if (immCommandPool) { vkDestroyCommandPool(device, immCommandPool, nullptr); immCommandPool = VK_NULL_HANDLE; }
|
||||||
|
if (transferCommandPool_) { vkDestroyCommandPool(device, transferCommandPool_, nullptr); transferCommandPool_ = VK_NULL_HANDLE; }
|
||||||
|
|
||||||
// Persist pipeline cache to disk before tearing down the device.
|
// Persist pipeline cache to disk before tearing down the device.
|
||||||
savePipelineCache();
|
savePipelineCache();
|
||||||
|
|
@ -97,6 +144,15 @@ void VkContext::shutdown() {
|
||||||
pipelineCache_ = VK_NULL_HANDLE;
|
pipelineCache_ = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destroy all cached samplers.
|
||||||
|
for (auto& [key, sampler] : samplerCache_) {
|
||||||
|
if (sampler) vkDestroySampler(device, sampler, nullptr);
|
||||||
|
}
|
||||||
|
samplerCache_.clear();
|
||||||
|
LOG_INFO("Sampler cache cleared");
|
||||||
|
|
||||||
|
sInstance_ = nullptr;
|
||||||
|
|
||||||
LOG_WARNING("VkContext::shutdown - destroySwapchain...");
|
LOG_WARNING("VkContext::shutdown - destroySwapchain...");
|
||||||
destroySwapchain();
|
destroySwapchain();
|
||||||
|
|
||||||
|
|
@ -135,6 +191,46 @@ void VkContext::runDeferredCleanup(uint32_t frameIndex) {
|
||||||
q.clear();
|
q.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkSampler VkContext::getOrCreateSampler(const VkSamplerCreateInfo& info) {
|
||||||
|
// Clamp anisotropy if the device doesn't support the feature.
|
||||||
|
VkSamplerCreateInfo adjusted = info;
|
||||||
|
if (!samplerAnisotropySupported_) {
|
||||||
|
adjusted.anisotropyEnable = VK_FALSE;
|
||||||
|
adjusted.maxAnisotropy = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t key = hashSamplerCreateInfo(adjusted);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(samplerCacheMutex_);
|
||||||
|
auto it = samplerCache_.find(key);
|
||||||
|
if (it != samplerCache_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new sampler outside the lock (vkCreateSampler is thread-safe
|
||||||
|
// for distinct create infos, but we re-lock to insert).
|
||||||
|
VkSampler sampler = VK_NULL_HANDLE;
|
||||||
|
if (vkCreateSampler(device, &adjusted, nullptr, &sampler) != VK_SUCCESS) {
|
||||||
|
LOG_ERROR("getOrCreateSampler: vkCreateSampler failed");
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(samplerCacheMutex_);
|
||||||
|
// Double-check: another thread may have inserted while we were creating.
|
||||||
|
auto [it, inserted] = samplerCache_.emplace(key, sampler);
|
||||||
|
if (!inserted) {
|
||||||
|
// Another thread won the race — destroy our duplicate and use theirs.
|
||||||
|
vkDestroySampler(device, sampler, nullptr);
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sampler;
|
||||||
|
}
|
||||||
|
|
||||||
bool VkContext::createInstance(SDL_Window* window) {
|
bool VkContext::createInstance(SDL_Window* window) {
|
||||||
// Get required SDL extensions
|
// Get required SDL extensions
|
||||||
unsigned int sdlExtCount = 0;
|
unsigned int sdlExtCount = 0;
|
||||||
|
|
@ -233,11 +329,52 @@ bool VkContext::selectPhysicalDevice() {
|
||||||
VK_VERSION_MINOR(props.apiVersion), ".", VK_VERSION_PATCH(props.apiVersion));
|
VK_VERSION_MINOR(props.apiVersion), ".", VK_VERSION_PATCH(props.apiVersion));
|
||||||
LOG_INFO("Depth resolve support: ", depthResolveSupported_ ? "YES" : "NO");
|
LOG_INFO("Depth resolve support: ", depthResolveSupported_ ? "YES" : "NO");
|
||||||
|
|
||||||
|
// Probe queue families to see if the graphics family supports multiple queues
|
||||||
|
// (used in createLogicalDevice to request a second queue for parallel uploads).
|
||||||
|
auto queueFamilies = vkbPhysicalDevice_.get_queue_families();
|
||||||
|
for (uint32_t i = 0; i < static_cast<uint32_t>(queueFamilies.size()); i++) {
|
||||||
|
if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
||||||
|
graphicsQueueFamilyQueueCount_ = queueFamilies[i].queueCount;
|
||||||
|
LOG_INFO("Graphics queue family ", i, " supports ", graphicsQueueFamilyQueueCount_, " queue(s)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VkContext::createLogicalDevice() {
|
bool VkContext::createLogicalDevice() {
|
||||||
vkb::DeviceBuilder deviceBuilder{vkbPhysicalDevice_};
|
vkb::DeviceBuilder deviceBuilder{vkbPhysicalDevice_};
|
||||||
|
|
||||||
|
// If the graphics queue family supports >= 2 queues, request a second one
|
||||||
|
// for parallel texture/buffer uploads. Both queues share the same family
|
||||||
|
// so no queue-ownership-transfer barriers are needed.
|
||||||
|
const bool requestTransferQueue = (graphicsQueueFamilyQueueCount_ >= 2);
|
||||||
|
|
||||||
|
if (requestTransferQueue) {
|
||||||
|
// Build a custom queue description list: 2 queues from the graphics
|
||||||
|
// family, 1 queue from every other family (so present etc. still work).
|
||||||
|
auto families = vkbPhysicalDevice_.get_queue_families();
|
||||||
|
uint32_t gfxFamily = UINT32_MAX;
|
||||||
|
for (uint32_t i = 0; i < static_cast<uint32_t>(families.size()); i++) {
|
||||||
|
if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
||||||
|
gfxFamily = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<vkb::CustomQueueDescription> queueDescs;
|
||||||
|
for (uint32_t i = 0; i < static_cast<uint32_t>(families.size()); i++) {
|
||||||
|
if (i == gfxFamily) {
|
||||||
|
// Request 2 queues: [0] graphics, [1] transfer uploads
|
||||||
|
queueDescs.emplace_back(i, std::vector<float>{1.0f, 1.0f});
|
||||||
|
} else {
|
||||||
|
queueDescs.emplace_back(i, std::vector<float>{1.0f});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deviceBuilder.custom_queue_setup(queueDescs);
|
||||||
|
}
|
||||||
|
|
||||||
auto devRet = deviceBuilder.build();
|
auto devRet = deviceBuilder.build();
|
||||||
if (!devRet) {
|
if (!devRet) {
|
||||||
LOG_ERROR("Failed to create Vulkan logical device: ", devRet.error().message());
|
LOG_ERROR("Failed to create Vulkan logical device: ", devRet.error().message());
|
||||||
|
|
@ -247,22 +384,45 @@ bool VkContext::createLogicalDevice() {
|
||||||
auto vkbDevice = devRet.value();
|
auto vkbDevice = devRet.value();
|
||||||
device = vkbDevice.device;
|
device = vkbDevice.device;
|
||||||
|
|
||||||
auto gqRet = vkbDevice.get_queue(vkb::QueueType::graphics);
|
if (requestTransferQueue) {
|
||||||
if (!gqRet) {
|
// With custom_queue_setup, we must retrieve queues manually.
|
||||||
LOG_ERROR("Failed to get graphics queue");
|
auto families = vkbPhysicalDevice_.get_queue_families();
|
||||||
return false;
|
uint32_t gfxFamily = UINT32_MAX;
|
||||||
}
|
for (uint32_t i = 0; i < static_cast<uint32_t>(families.size()); i++) {
|
||||||
graphicsQueue = gqRet.value();
|
if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
|
||||||
graphicsQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::graphics).value();
|
gfxFamily = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graphicsQueueFamily = gfxFamily;
|
||||||
|
vkGetDeviceQueue(device, gfxFamily, 0, &graphicsQueue);
|
||||||
|
vkGetDeviceQueue(device, gfxFamily, 1, &transferQueue_);
|
||||||
|
hasDedicatedTransfer_ = true;
|
||||||
|
|
||||||
auto pqRet = vkbDevice.get_queue(vkb::QueueType::present);
|
// Present queue: try the graphics family first (most common), otherwise
|
||||||
if (!pqRet) {
|
// find a family that supports presentation.
|
||||||
// Fall back to graphics queue for presentation
|
|
||||||
presentQueue = graphicsQueue;
|
presentQueue = graphicsQueue;
|
||||||
presentQueueFamily = graphicsQueueFamily;
|
presentQueueFamily = gfxFamily;
|
||||||
|
|
||||||
|
LOG_INFO("Dedicated transfer queue enabled (family ", gfxFamily, ", queue index 1)");
|
||||||
} else {
|
} else {
|
||||||
presentQueue = pqRet.value();
|
// Standard path — let vkb resolve queues.
|
||||||
presentQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::present).value();
|
auto gqRet = vkbDevice.get_queue(vkb::QueueType::graphics);
|
||||||
|
if (!gqRet) {
|
||||||
|
LOG_ERROR("Failed to get graphics queue");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
graphicsQueue = gqRet.value();
|
||||||
|
graphicsQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::graphics).value();
|
||||||
|
|
||||||
|
auto pqRet = vkbDevice.get_queue(vkb::QueueType::present);
|
||||||
|
if (!pqRet) {
|
||||||
|
presentQueue = graphicsQueue;
|
||||||
|
presentQueueFamily = graphicsQueueFamily;
|
||||||
|
} else {
|
||||||
|
presentQueue = pqRet.value();
|
||||||
|
presentQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::present).value();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Vulkan logical device created");
|
LOG_INFO("Vulkan logical device created");
|
||||||
|
|
@ -493,6 +653,19 @@ bool VkContext::createCommandPools() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Separate command pool for the transfer queue (same family, different queue)
|
||||||
|
if (hasDedicatedTransfer_) {
|
||||||
|
VkCommandPoolCreateInfo transferPoolInfo{};
|
||||||
|
transferPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
||||||
|
transferPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||||
|
transferPoolInfo.queueFamilyIndex = graphicsQueueFamily;
|
||||||
|
|
||||||
|
if (vkCreateCommandPool(device, &transferPoolInfo, nullptr, &transferCommandPool_) != VK_SUCCESS) {
|
||||||
|
LOG_ERROR("Failed to create transfer command pool");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -980,10 +1153,7 @@ void VkContext::destroyImGuiResources() {
|
||||||
if (tex.memory) vkFreeMemory(device, tex.memory, nullptr);
|
if (tex.memory) vkFreeMemory(device, tex.memory, nullptr);
|
||||||
}
|
}
|
||||||
uiTextures_.clear();
|
uiTextures_.clear();
|
||||||
if (uiTextureSampler_) {
|
uiTextureSampler_ = VK_NULL_HANDLE; // Owned by sampler cache
|
||||||
vkDestroySampler(device, uiTextureSampler_, nullptr);
|
|
||||||
uiTextureSampler_ = VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imguiDescriptorPool) {
|
if (imguiDescriptorPool) {
|
||||||
vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr);
|
vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr);
|
||||||
|
|
@ -1015,7 +1185,7 @@ VkDescriptorSet VkContext::uploadImGuiTexture(const uint8_t* rgba, int width, in
|
||||||
|
|
||||||
VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * height * 4;
|
VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * height * 4;
|
||||||
|
|
||||||
// Create shared sampler on first call
|
// Create shared sampler on first call (via sampler cache)
|
||||||
if (!uiTextureSampler_) {
|
if (!uiTextureSampler_) {
|
||||||
VkSamplerCreateInfo si{};
|
VkSamplerCreateInfo si{};
|
||||||
si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||||
|
|
@ -1024,7 +1194,8 @@ VkDescriptorSet VkContext::uploadImGuiTexture(const uint8_t* rgba, int width, in
|
||||||
si.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
si.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
si.addressModeV = 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;
|
si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
if (vkCreateSampler(device, &si, nullptr, &uiTextureSampler_) != VK_SUCCESS) {
|
uiTextureSampler_ = getOrCreateSampler(si);
|
||||||
|
if (!uiTextureSampler_) {
|
||||||
LOG_ERROR("Failed to create UI texture sampler");
|
LOG_ERROR("Failed to create UI texture sampler");
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
|
|
@ -1616,7 +1787,21 @@ void VkContext::beginUploadBatch() {
|
||||||
uploadBatchDepth_++;
|
uploadBatchDepth_++;
|
||||||
if (inUploadBatch_) return; // already in a batch (nested call)
|
if (inUploadBatch_) return; // already in a batch (nested call)
|
||||||
inUploadBatch_ = true;
|
inUploadBatch_ = true;
|
||||||
batchCmd_ = beginSingleTimeCommands();
|
|
||||||
|
// Allocate from transfer pool if available, otherwise from immCommandPool.
|
||||||
|
VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool;
|
||||||
|
|
||||||
|
VkCommandBufferAllocateInfo allocInfo{};
|
||||||
|
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||||
|
allocInfo.commandPool = pool;
|
||||||
|
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||||
|
allocInfo.commandBufferCount = 1;
|
||||||
|
vkAllocateCommandBuffers(device, &allocInfo, &batchCmd_);
|
||||||
|
|
||||||
|
VkCommandBufferBeginInfo beginInfo{};
|
||||||
|
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||||
|
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||||
|
vkBeginCommandBuffer(batchCmd_, &beginInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkContext::endUploadBatch() {
|
void VkContext::endUploadBatch() {
|
||||||
|
|
@ -1626,10 +1811,12 @@ void VkContext::endUploadBatch() {
|
||||||
|
|
||||||
inUploadBatch_ = false;
|
inUploadBatch_ = false;
|
||||||
|
|
||||||
|
VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool;
|
||||||
|
|
||||||
if (batchStagingBuffers_.empty()) {
|
if (batchStagingBuffers_.empty()) {
|
||||||
// No GPU copies were recorded — skip the submit entirely.
|
// No GPU copies were recorded — skip the submit entirely.
|
||||||
vkEndCommandBuffer(batchCmd_);
|
vkEndCommandBuffer(batchCmd_);
|
||||||
vkFreeCommandBuffers(device, immCommandPool, 1, &batchCmd_);
|
vkFreeCommandBuffers(device, pool, 1, &batchCmd_);
|
||||||
batchCmd_ = VK_NULL_HANDLE;
|
batchCmd_ = VK_NULL_HANDLE;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1646,7 +1833,10 @@ void VkContext::endUploadBatch() {
|
||||||
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||||
submitInfo.commandBufferCount = 1;
|
submitInfo.commandBufferCount = 1;
|
||||||
submitInfo.pCommandBuffers = &batchCmd_;
|
submitInfo.pCommandBuffers = &batchCmd_;
|
||||||
vkQueueSubmit(graphicsQueue, 1, &submitInfo, fence);
|
|
||||||
|
// Submit to the dedicated transfer queue if available, otherwise graphics.
|
||||||
|
VkQueue targetQueue = hasDedicatedTransfer_ ? transferQueue_ : graphicsQueue;
|
||||||
|
vkQueueSubmit(targetQueue, 1, &submitInfo, fence);
|
||||||
|
|
||||||
// Stash everything for later cleanup when fence signals
|
// Stash everything for later cleanup when fence signals
|
||||||
InFlightBatch batch;
|
InFlightBatch batch;
|
||||||
|
|
@ -1666,15 +1856,30 @@ void VkContext::endUploadBatchSync() {
|
||||||
|
|
||||||
inUploadBatch_ = false;
|
inUploadBatch_ = false;
|
||||||
|
|
||||||
|
VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool;
|
||||||
|
|
||||||
if (batchStagingBuffers_.empty()) {
|
if (batchStagingBuffers_.empty()) {
|
||||||
vkEndCommandBuffer(batchCmd_);
|
vkEndCommandBuffer(batchCmd_);
|
||||||
vkFreeCommandBuffers(device, immCommandPool, 1, &batchCmd_);
|
vkFreeCommandBuffers(device, pool, 1, &batchCmd_);
|
||||||
batchCmd_ = VK_NULL_HANDLE;
|
batchCmd_ = VK_NULL_HANDLE;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synchronous path for load screens — submit and wait
|
// Synchronous path for load screens — submit and wait on the target queue.
|
||||||
endSingleTimeCommands(batchCmd_);
|
VkQueue targetQueue = hasDedicatedTransfer_ ? transferQueue_ : graphicsQueue;
|
||||||
|
|
||||||
|
vkEndCommandBuffer(batchCmd_);
|
||||||
|
|
||||||
|
VkSubmitInfo submitInfo{};
|
||||||
|
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||||
|
submitInfo.commandBufferCount = 1;
|
||||||
|
submitInfo.pCommandBuffers = &batchCmd_;
|
||||||
|
|
||||||
|
vkQueueSubmit(targetQueue, 1, &submitInfo, immFence);
|
||||||
|
vkWaitForFences(device, 1, &immFence, VK_TRUE, UINT64_MAX);
|
||||||
|
vkResetFences(device, 1, &immFence);
|
||||||
|
|
||||||
|
vkFreeCommandBuffers(device, pool, 1, &batchCmd_);
|
||||||
batchCmd_ = VK_NULL_HANDLE;
|
batchCmd_ = VK_NULL_HANDLE;
|
||||||
|
|
||||||
for (auto& staging : batchStagingBuffers_) {
|
for (auto& staging : batchStagingBuffers_) {
|
||||||
|
|
@ -1686,6 +1891,8 @@ void VkContext::endUploadBatchSync() {
|
||||||
void VkContext::pollUploadBatches() {
|
void VkContext::pollUploadBatches() {
|
||||||
if (inFlightBatches_.empty()) return;
|
if (inFlightBatches_.empty()) return;
|
||||||
|
|
||||||
|
VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool;
|
||||||
|
|
||||||
for (auto it = inFlightBatches_.begin(); it != inFlightBatches_.end(); ) {
|
for (auto it = inFlightBatches_.begin(); it != inFlightBatches_.end(); ) {
|
||||||
VkResult result = vkGetFenceStatus(device, it->fence);
|
VkResult result = vkGetFenceStatus(device, it->fence);
|
||||||
if (result == VK_SUCCESS) {
|
if (result == VK_SUCCESS) {
|
||||||
|
|
@ -1693,7 +1900,7 @@ void VkContext::pollUploadBatches() {
|
||||||
for (auto& staging : it->stagingBuffers) {
|
for (auto& staging : it->stagingBuffers) {
|
||||||
destroyBuffer(allocator, staging);
|
destroyBuffer(allocator, staging);
|
||||||
}
|
}
|
||||||
vkFreeCommandBuffers(device, immCommandPool, 1, &it->cmd);
|
vkFreeCommandBuffers(device, pool, 1, &it->cmd);
|
||||||
vkDestroyFence(device, it->fence, nullptr);
|
vkDestroyFence(device, it->fence, nullptr);
|
||||||
it = inFlightBatches_.erase(it);
|
it = inFlightBatches_.erase(it);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1703,12 +1910,14 @@ void VkContext::pollUploadBatches() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkContext::waitAllUploads() {
|
void VkContext::waitAllUploads() {
|
||||||
|
VkCommandPool pool = hasDedicatedTransfer_ ? transferCommandPool_ : immCommandPool;
|
||||||
|
|
||||||
for (auto& batch : inFlightBatches_) {
|
for (auto& batch : inFlightBatches_) {
|
||||||
vkWaitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX);
|
vkWaitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX);
|
||||||
for (auto& staging : batch.stagingBuffers) {
|
for (auto& staging : batch.stagingBuffers) {
|
||||||
destroyBuffer(allocator, staging);
|
destroyBuffer(allocator, staging);
|
||||||
}
|
}
|
||||||
vkFreeCommandBuffers(device, immCommandPool, 1, &batch.cmd);
|
vkFreeCommandBuffers(device, pool, 1, &batch.cmd);
|
||||||
vkDestroyFence(device, batch.fence, nullptr);
|
vkDestroyFence(device, batch.fence, nullptr);
|
||||||
}
|
}
|
||||||
inFlightBatches_.clear();
|
inFlightBatches_.clear();
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create sampler (linear filtering, clamp to edge)
|
// Create sampler (linear filtering, clamp to edge) via cache
|
||||||
VkSamplerCreateInfo samplerInfo{};
|
VkSamplerCreateInfo samplerInfo{};
|
||||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||||
samplerInfo.minFilter = VK_FILTER_LINEAR;
|
samplerInfo.minFilter = VK_FILTER_LINEAR;
|
||||||
|
|
@ -61,11 +61,13 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height,
|
||||||
samplerInfo.minLod = 0.0f;
|
samplerInfo.minLod = 0.0f;
|
||||||
samplerInfo.maxLod = 0.0f;
|
samplerInfo.maxLod = 0.0f;
|
||||||
|
|
||||||
if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) {
|
sampler_ = ctx.getOrCreateSampler(samplerInfo);
|
||||||
|
if (sampler_ == VK_NULL_HANDLE) {
|
||||||
LOG_ERROR("VkRenderTarget: failed to create sampler");
|
LOG_ERROR("VkRenderTarget: failed to create sampler");
|
||||||
destroy(device, allocator);
|
destroy(device, allocator);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
ownsSampler_ = false;
|
||||||
|
|
||||||
// Create render pass
|
// Create render pass
|
||||||
if (useMSAA) {
|
if (useMSAA) {
|
||||||
|
|
@ -259,10 +261,11 @@ void VkRenderTarget::destroy(VkDevice device, VmaAllocator allocator) {
|
||||||
vkDestroyRenderPass(device, renderPass_, nullptr);
|
vkDestroyRenderPass(device, renderPass_, nullptr);
|
||||||
renderPass_ = VK_NULL_HANDLE;
|
renderPass_ = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (sampler_) {
|
if (sampler_ && ownsSampler_) {
|
||||||
vkDestroySampler(device, sampler_, nullptr);
|
vkDestroySampler(device, sampler_, nullptr);
|
||||||
sampler_ = VK_NULL_HANDLE;
|
|
||||||
}
|
}
|
||||||
|
sampler_ = VK_NULL_HANDLE;
|
||||||
|
ownsSampler_ = true;
|
||||||
destroyImage(device, allocator, resolveImage_);
|
destroyImage(device, allocator, resolveImage_);
|
||||||
destroyImage(device, allocator, depthImage_);
|
destroyImage(device, allocator, depthImage_);
|
||||||
destroyImage(device, allocator, colorImage_);
|
destroyImage(device, allocator, colorImage_);
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ VkTexture::~VkTexture() {
|
||||||
}
|
}
|
||||||
|
|
||||||
VkTexture::VkTexture(VkTexture&& other) noexcept
|
VkTexture::VkTexture(VkTexture&& other) noexcept
|
||||||
: image_(other.image_), sampler_(other.sampler_), mipLevels_(other.mipLevels_) {
|
: image_(other.image_), sampler_(other.sampler_), mipLevels_(other.mipLevels_),
|
||||||
|
ownsSampler_(other.ownsSampler_) {
|
||||||
other.image_ = {};
|
other.image_ = {};
|
||||||
other.sampler_ = VK_NULL_HANDLE;
|
other.sampler_ = VK_NULL_HANDLE;
|
||||||
|
other.ownsSampler_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkTexture& VkTexture::operator=(VkTexture&& other) noexcept {
|
VkTexture& VkTexture::operator=(VkTexture&& other) noexcept {
|
||||||
|
|
@ -23,8 +25,10 @@ VkTexture& VkTexture::operator=(VkTexture&& other) noexcept {
|
||||||
image_ = other.image_;
|
image_ = other.image_;
|
||||||
sampler_ = other.sampler_;
|
sampler_ = other.sampler_;
|
||||||
mipLevels_ = other.mipLevels_;
|
mipLevels_ = other.mipLevels_;
|
||||||
|
ownsSampler_ = other.ownsSampler_;
|
||||||
other.image_ = {};
|
other.image_ = {};
|
||||||
other.sampler_ = VK_NULL_HANDLE;
|
other.sampler_ = VK_NULL_HANDLE;
|
||||||
|
other.ownsSampler_ = true;
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
@ -214,11 +218,20 @@ bool VkTexture::createSampler(VkDevice device,
|
||||||
samplerInfo.minLod = 0.0f;
|
samplerInfo.minLod = 0.0f;
|
||||||
samplerInfo.maxLod = static_cast<float>(mipLevels_);
|
samplerInfo.maxLod = static_cast<float>(mipLevels_);
|
||||||
|
|
||||||
|
// Use sampler cache if VkContext is available.
|
||||||
|
auto* ctx = VkContext::globalInstance();
|
||||||
|
if (ctx) {
|
||||||
|
sampler_ = ctx->getOrCreateSampler(samplerInfo);
|
||||||
|
ownsSampler_ = false;
|
||||||
|
return sampler_ != VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: no VkContext (shouldn't happen in normal use).
|
||||||
if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) {
|
if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) {
|
||||||
LOG_ERROR("Failed to create texture sampler");
|
LOG_ERROR("Failed to create texture sampler");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
ownsSampler_ = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,11 +259,20 @@ bool VkTexture::createSampler(VkDevice device,
|
||||||
samplerInfo.minLod = 0.0f;
|
samplerInfo.minLod = 0.0f;
|
||||||
samplerInfo.maxLod = static_cast<float>(mipLevels_);
|
samplerInfo.maxLod = static_cast<float>(mipLevels_);
|
||||||
|
|
||||||
|
// Use sampler cache if VkContext is available.
|
||||||
|
auto* ctx = VkContext::globalInstance();
|
||||||
|
if (ctx) {
|
||||||
|
sampler_ = ctx->getOrCreateSampler(samplerInfo);
|
||||||
|
ownsSampler_ = false;
|
||||||
|
return sampler_ != VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: no VkContext (shouldn't happen in normal use).
|
||||||
if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) {
|
if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) {
|
||||||
LOG_ERROR("Failed to create texture sampler");
|
LOG_ERROR("Failed to create texture sampler");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
ownsSampler_ = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,19 +291,29 @@ bool VkTexture::createShadowSampler(VkDevice device) {
|
||||||
samplerInfo.minLod = 0.0f;
|
samplerInfo.minLod = 0.0f;
|
||||||
samplerInfo.maxLod = 1.0f;
|
samplerInfo.maxLod = 1.0f;
|
||||||
|
|
||||||
|
// Use sampler cache if VkContext is available.
|
||||||
|
auto* ctx = VkContext::globalInstance();
|
||||||
|
if (ctx) {
|
||||||
|
sampler_ = ctx->getOrCreateSampler(samplerInfo);
|
||||||
|
ownsSampler_ = false;
|
||||||
|
return sampler_ != VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: no VkContext (shouldn't happen in normal use).
|
||||||
if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) {
|
if (vkCreateSampler(device, &samplerInfo, nullptr, &sampler_) != VK_SUCCESS) {
|
||||||
LOG_ERROR("Failed to create shadow sampler");
|
LOG_ERROR("Failed to create shadow sampler");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
ownsSampler_ = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkTexture::destroy(VkDevice device, VmaAllocator allocator) {
|
void VkTexture::destroy(VkDevice device, VmaAllocator allocator) {
|
||||||
if (sampler_ != VK_NULL_HANDLE) {
|
if (sampler_ != VK_NULL_HANDLE && ownsSampler_) {
|
||||||
vkDestroySampler(device, sampler_, nullptr);
|
vkDestroySampler(device, sampler_, nullptr);
|
||||||
sampler_ = VK_NULL_HANDLE;
|
|
||||||
}
|
}
|
||||||
|
sampler_ = VK_NULL_HANDLE;
|
||||||
|
ownsSampler_ = true;
|
||||||
destroyImage(device, allocator, image_);
|
destroyImage(device, allocator, image_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -352,8 +352,8 @@ void WaterRenderer::destroySceneHistoryResources() {
|
||||||
if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; }
|
if (sh.depthImage) { vmaDestroyImage(vkCtx->getAllocator(), sh.depthImage, sh.depthAlloc); sh.depthImage = VK_NULL_HANDLE; sh.depthAlloc = VK_NULL_HANDLE; }
|
||||||
sh.sceneSet = VK_NULL_HANDLE;
|
sh.sceneSet = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (sceneColorSampler) { vkDestroySampler(device, sceneColorSampler, nullptr); sceneColorSampler = VK_NULL_HANDLE; }
|
sceneColorSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
if (sceneDepthSampler) { vkDestroySampler(device, sceneDepthSampler, nullptr); sceneDepthSampler = VK_NULL_HANDLE; }
|
sceneDepthSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
sceneHistoryExtent = {0, 0};
|
sceneHistoryExtent = {0, 0};
|
||||||
sceneHistoryReady = false;
|
sceneHistoryReady = false;
|
||||||
}
|
}
|
||||||
|
|
@ -374,13 +374,15 @@ void WaterRenderer::createSceneHistoryResources(VkExtent2D extent, VkFormat colo
|
||||||
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
if (vkCreateSampler(device, &sampCI, nullptr, &sceneColorSampler) != VK_SUCCESS) {
|
sceneColorSampler = vkCtx->getOrCreateSampler(sampCI);
|
||||||
|
if (sceneColorSampler == VK_NULL_HANDLE) {
|
||||||
LOG_ERROR("WaterRenderer: failed to create scene color sampler");
|
LOG_ERROR("WaterRenderer: failed to create scene color sampler");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sampCI.magFilter = VK_FILTER_NEAREST;
|
sampCI.magFilter = VK_FILTER_NEAREST;
|
||||||
sampCI.minFilter = VK_FILTER_NEAREST;
|
sampCI.minFilter = VK_FILTER_NEAREST;
|
||||||
if (vkCreateSampler(device, &sampCI, nullptr, &sceneDepthSampler) != VK_SUCCESS) {
|
sceneDepthSampler = vkCtx->getOrCreateSampler(sampCI);
|
||||||
|
if (sceneDepthSampler == VK_NULL_HANDLE) {
|
||||||
LOG_ERROR("WaterRenderer: failed to create scene depth sampler");
|
LOG_ERROR("WaterRenderer: failed to create scene depth sampler");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1718,7 +1720,8 @@ void WaterRenderer::createReflectionResources() {
|
||||||
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
sampCI.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
sampCI.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
sampCI.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
if (vkCreateSampler(device, &sampCI, nullptr, &reflectionSampler) != VK_SUCCESS) {
|
reflectionSampler = vkCtx->getOrCreateSampler(sampCI);
|
||||||
|
if (reflectionSampler == VK_NULL_HANDLE) {
|
||||||
LOG_ERROR("WaterRenderer: failed to create reflection sampler");
|
LOG_ERROR("WaterRenderer: failed to create reflection sampler");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1848,7 +1851,7 @@ void WaterRenderer::destroyReflectionResources() {
|
||||||
if (reflectionDepthView) { vkDestroyImageView(device, reflectionDepthView, nullptr); reflectionDepthView = VK_NULL_HANDLE; }
|
if (reflectionDepthView) { vkDestroyImageView(device, reflectionDepthView, nullptr); reflectionDepthView = VK_NULL_HANDLE; }
|
||||||
if (reflectionColorImage) { vmaDestroyImage(allocator, reflectionColorImage, reflectionColorAlloc); reflectionColorImage = VK_NULL_HANDLE; }
|
if (reflectionColorImage) { vmaDestroyImage(allocator, reflectionColorImage, reflectionColorAlloc); reflectionColorImage = VK_NULL_HANDLE; }
|
||||||
if (reflectionDepthImage) { vmaDestroyImage(allocator, reflectionDepthImage, reflectionDepthAlloc); reflectionDepthImage = VK_NULL_HANDLE; }
|
if (reflectionDepthImage) { vmaDestroyImage(allocator, reflectionDepthImage, reflectionDepthAlloc); reflectionDepthImage = VK_NULL_HANDLE; }
|
||||||
if (reflectionSampler) { vkDestroySampler(device, reflectionSampler, nullptr); reflectionSampler = VK_NULL_HANDLE; }
|
reflectionSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
if (reflectionUBO) {
|
if (reflectionUBO) {
|
||||||
AllocatedBuffer ab{}; ab.buffer = reflectionUBO; ab.allocation = reflectionUBOAlloc;
|
AllocatedBuffer ab{}; ab.buffer = reflectionUBO; ab.allocation = reflectionUBOAlloc;
|
||||||
destroyBuffer(allocator, ab);
|
destroyBuffer(allocator, ab);
|
||||||
|
|
|
||||||
|
|
@ -915,7 +915,7 @@ bool AuthScreen::loadBackgroundImage() {
|
||||||
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||||
vkCreateSampler(device, &samplerInfo, nullptr, &bgSampler);
|
bgSampler = bgVkCtx->getOrCreateSampler(samplerInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
bgDescriptorSet = ImGui_ImplVulkan_AddTexture(bgSampler, bgImageView,
|
bgDescriptorSet = ImGui_ImplVulkan_AddTexture(bgSampler, bgImageView,
|
||||||
|
|
@ -930,7 +930,7 @@ void AuthScreen::destroyBackgroundImage() {
|
||||||
VkDevice device = bgVkCtx->getDevice();
|
VkDevice device = bgVkCtx->getDevice();
|
||||||
vkDeviceWaitIdle(device);
|
vkDeviceWaitIdle(device);
|
||||||
if (bgDescriptorSet) { ImGui_ImplVulkan_RemoveTexture(bgDescriptorSet); bgDescriptorSet = VK_NULL_HANDLE; }
|
if (bgDescriptorSet) { ImGui_ImplVulkan_RemoveTexture(bgDescriptorSet); bgDescriptorSet = VK_NULL_HANDLE; }
|
||||||
if (bgSampler) { vkDestroySampler(device, bgSampler, nullptr); bgSampler = VK_NULL_HANDLE; }
|
bgSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
|
||||||
if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; }
|
if (bgImageView) { vkDestroyImageView(device, bgImageView, nullptr); bgImageView = VK_NULL_HANDLE; }
|
||||||
if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; }
|
if (bgImage) { vkDestroyImage(device, bgImage, nullptr); bgImage = VK_NULL_HANDLE; }
|
||||||
if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; }
|
if (bgMemory) { vkFreeMemory(device, bgMemory, nullptr); bgMemory = VK_NULL_HANDLE; }
|
||||||
|
|
|
||||||
|
|
@ -17436,6 +17436,12 @@ void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
||||||
gameHandler.startCraftQueue(selectedCraftSpell, craftQuantity);
|
gameHandler.startCraftQueue(selectedCraftSpell, craftQuantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Create All")) {
|
||||||
|
// Queue a large count — server stops the queue automatically
|
||||||
|
// when materials run out (sends SPELL_FAILED_REAGENTS).
|
||||||
|
gameHandler.startCraftQueue(selectedCraftSpell, 999);
|
||||||
|
}
|
||||||
if (!canCraft) ImGui::EndDisabled();
|
if (!canCraft) ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue