diff --git a/include/rendering/character_preview.hpp b/include/rendering/character_preview.hpp index 40924ed9..079c299c 100644 --- a/include/rendering/character_preview.hpp +++ b/include/rendering/character_preview.hpp @@ -2,6 +2,7 @@ #include "game/character.hpp" #include +#include #include #include #include @@ -15,6 +16,7 @@ class CharacterRenderer; class Camera; class VkContext; class VkTexture; +class VkRenderTarget; class CharacterPreview { public: @@ -36,8 +38,15 @@ public: void render(); void rotate(float yawDelta); - // TODO: Vulkan offscreen render target for preview - VkTexture* getTextureId() const { return nullptr; } + // Off-screen composite pass — call from Renderer::beginFrame() before main render pass + void compositePass(VkCommandBuffer cmd, uint32_t frameIndex); + + // Mark that the preview needs compositing this frame (call from UI each frame) + void requestComposite() { compositeRequested_ = true; } + + // Returns the ImGui texture handle. Returns VK_NULL_HANDLE until the first + // compositePass has run (image is in UNDEFINED layout before that). + VkDescriptorSet getTextureId() const { return compositeRendered_ ? imguiTextureId_ : VK_NULL_HANDLE; } int getWidth() const { return fboWidth_; } int getHeight() const { return fboHeight_; } @@ -51,17 +60,35 @@ private: void destroyFBO(); pipeline::AssetManager* assetManager_ = nullptr; + VkContext* vkCtx_ = nullptr; std::unique_ptr charRenderer_; std::unique_ptr camera_; - // TODO: Vulkan offscreen render target - // VkRenderTarget* renderTarget_ = nullptr; + // Off-screen render target (color + depth) + std::unique_ptr renderTarget_; + + // Per-frame UBO for preview camera/lighting (double-buffered) + static constexpr uint32_t MAX_FRAMES = 2; + VkDescriptorPool previewDescPool_ = VK_NULL_HANDLE; + VkBuffer previewUBO_[MAX_FRAMES] = {}; + VmaAllocation previewUBOAlloc_[MAX_FRAMES] = {}; + void* previewUBOMapped_[MAX_FRAMES] = {}; + VkDescriptorSet previewPerFrameSet_[MAX_FRAMES] = {}; + + // Dummy 1x1 white texture for shadow map placeholder + std::unique_ptr dummyWhiteTex_; + + // ImGui texture handle for displaying the preview (VkDescriptorSet in Vulkan backend) + VkDescriptorSet imguiTextureId_ = VK_NULL_HANDLE; + static constexpr int fboWidth_ = 400; static constexpr int fboHeight_ = 500; static constexpr uint32_t PREVIEW_MODEL_ID = 9999; uint32_t instanceId_ = 0; bool modelLoaded_ = false; + bool compositeRequested_ = false; + bool compositeRendered_ = false; // True after first successful compositePass float modelYaw_ = 180.0f; // Cached info from loadCharacter() for later recompositing. diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 1ef0f24c..ed7f81ca 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -42,7 +42,8 @@ public: CharacterRenderer(); ~CharacterRenderer(); - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am); + bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am, + VkRenderPass renderPassOverride = VK_NULL_HANDLE); void shutdown(); void setAssetManager(pipeline::AssetManager* am) { assetManager = am; } @@ -219,7 +220,9 @@ public: private: VkContext* vkCtx_ = nullptr; + VkRenderPass renderPassOverride_ = VK_NULL_HANDLE; pipeline::AssetManager* assetManager = nullptr; + int renderLogCounter_ = 0; // per-instance debug counter // Vulkan pipelines (one per blend mode) VkPipeline opaquePipeline_ = VK_NULL_HANDLE; diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index a8671311..2df22e05 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -43,6 +43,7 @@ class M2Renderer; class Minimap; class WorldMap; class QuestMarkerRenderer; +class CharacterPreview; class Shader; class Renderer { @@ -239,6 +240,10 @@ private: int shadowPostMoveFrames_ = 0; // transition marker for movement->idle shadow recenter public: + // Character preview registration (for off-screen composite pass) + void registerPreview(CharacterPreview* preview); + void unregisterPreview(CharacterPreview* preview); + void setShadowsEnabled(bool enabled) { shadowsEnabled = enabled; } bool areShadowsEnabled() const { return shadowsEnabled; } void setMsaaSamples(VkSampleCountFlagBits samples); @@ -384,6 +389,9 @@ private: void destroyPerFrameResources(); void updatePerFrameUBO(); + // Active character previews for off-screen rendering + std::vector activePreviews_; + bool terrainEnabled = true; bool terrainLoaded = false; diff --git a/include/rendering/vk_render_target.hpp b/include/rendering/vk_render_target.hpp index ef29620f..3acbc561 100644 --- a/include/rendering/vk_render_target.hpp +++ b/include/rendering/vk_render_target.hpp @@ -25,9 +25,10 @@ public: /** * Create the render target with given dimensions and format. * Creates: color image, image view, sampler, render pass, framebuffer. + * When withDepth is true, also creates a D32_SFLOAT depth attachment. */ bool create(VkContext& ctx, uint32_t width, uint32_t height, - VkFormat format = VK_FORMAT_R8G8B8A8_UNORM); + VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, bool withDepth = false); /** * Destroy all Vulkan resources. @@ -48,6 +49,7 @@ public: void endPass(VkCommandBuffer cmd); // Accessors + VkImage getColorImage() const { return colorImage_.image; } VkImageView getColorImageView() const { return colorImage_.imageView; } VkSampler getSampler() const { return sampler_; } VkRenderPass getRenderPass() const { return renderPass_; } @@ -62,6 +64,8 @@ public: private: AllocatedImage colorImage_{}; + AllocatedImage depthImage_{}; + bool hasDepth_ = false; VkSampler sampler_ = VK_NULL_HANDLE; VkRenderPass renderPass_ = VK_NULL_HANDLE; VkFramebuffer framebuffer_ = VK_NULL_HANDLE; diff --git a/src/core/application.cpp b/src/core/application.cpp index 63fa2039..1cdcdb31 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -336,8 +336,9 @@ void Application::run() { totalSwapMs += std::chrono::duration(t4 - t3).count(); if (++frameCount >= 60) { - printf("[Frame] Update: %.1f ms, Render: %.1f ms, Swap: %.1f ms\n", - totalUpdateMs / 60.0, totalRenderMs / 60.0, totalSwapMs / 60.0); + LOG_INFO("[Frame] Update: ", totalUpdateMs / 60.0, + "ms Render: ", totalRenderMs / 60.0, + "ms Swap: ", totalSwapMs / 60.0, "ms"); frameCount = 0; totalUpdateMs = totalRenderMs = totalSwapMs = 0; } @@ -358,9 +359,12 @@ void Application::shutdown() { } } - // Stop renderer first: terrain streaming workers may still be reading via - // AssetManager during shutdown, so renderer/terrain teardown must complete - // before AssetManager is destroyed. + // Explicitly shut down the renderer before destroying it — this ensures + // all sub-renderers free their VMA allocations in the correct order, + // before VkContext::shutdown() calls vmaDestroyAllocator(). + if (renderer) { + renderer->shutdown(); + } renderer.reset(); world.reset(); diff --git a/src/rendering/camera.cpp b/src/rendering/camera.cpp index f2edc7a7..f8b45f3c 100644 --- a/src/rendering/camera.cpp +++ b/src/rendering/camera.cpp @@ -42,12 +42,15 @@ glm::vec3 Camera::getUp() const { Ray Camera::screenToWorldRay(float screenX, float screenY, float screenW, float screenH) const { float ndcX = (2.0f * screenX / screenW) - 1.0f; - float ndcY = 1.0f - (2.0f * screenY / screenH); + // Vulkan Y-flip is baked into projectionMatrix, so NDC Y maps directly: + // screen top (y=0) → NDC -1, screen bottom (y=H) → NDC +1 + float ndcY = (2.0f * screenY / screenH) - 1.0f; glm::mat4 invVP = glm::inverse(projectionMatrix * viewMatrix); - glm::vec4 nearPt = invVP * glm::vec4(ndcX, ndcY, -1.0f, 1.0f); - glm::vec4 farPt = invVP * glm::vec4(ndcX, ndcY, 1.0f, 1.0f); + // Vulkan / GLM_FORCE_DEPTH_ZERO_TO_ONE: NDC z ∈ [0, 1] + glm::vec4 nearPt = invVP * glm::vec4(ndcX, ndcY, 0.0f, 1.0f); + glm::vec4 farPt = invVP * glm::vec4(ndcX, ndcY, 1.0f, 1.0f); nearPt /= nearPt.w; farPt /= farPt.w; diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 9ceec954..8288f81b 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -1,7 +1,9 @@ #include "rendering/character_preview.hpp" #include "rendering/character_renderer.hpp" +#include "rendering/vk_render_target.hpp" #include "rendering/vk_texture.hpp" #include "rendering/vk_context.hpp" +#include "rendering/vk_frame_data.hpp" #include "rendering/camera.hpp" #include "rendering/renderer.hpp" #include "pipeline/asset_manager.hpp" @@ -10,9 +12,12 @@ #include "pipeline/dbc_layout.hpp" #include "core/logger.hpp" #include "core/application.hpp" +#include +#include #include #include #include +#include namespace wowee { namespace rendering { @@ -26,11 +31,34 @@ CharacterPreview::~CharacterPreview() { bool CharacterPreview::initialize(pipeline::AssetManager* am) { assetManager_ = am; - charRenderer_ = std::make_unique(); + // If already initialized with valid resources, reuse them. + // This avoids destroying GPU resources that may still be referenced by + // an in-flight command buffer (compositePass recorded earlier this frame). + if (renderTarget_ && renderTarget_->isValid() && charRenderer_ && camera_) { + // Mark model as not loaded — loadCharacter() will handle instance cleanup + modelLoaded_ = false; + return true; + } + auto* appRenderer = core::Application::getInstance().getRenderer(); - VkContext* vkCtx = appRenderer ? appRenderer->getVkContext() : nullptr; + vkCtx_ = appRenderer ? appRenderer->getVkContext() : nullptr; VkDescriptorSetLayout perFrameLayout = appRenderer ? appRenderer->getPerFrameSetLayout() : VK_NULL_HANDLE; - if (!charRenderer_->initialize(vkCtx, perFrameLayout, am)) { + + if (!vkCtx_ || perFrameLayout == VK_NULL_HANDLE) { + LOG_ERROR("CharacterPreview: no VkContext or perFrameLayout available"); + return false; + } + + // Create off-screen render target first (need its render pass for pipeline creation) + createFBO(); + if (!renderTarget_ || !renderTarget_->isValid()) { + LOG_ERROR("CharacterPreview: failed to create off-screen render target"); + return false; + } + + // Initialize CharacterRenderer with our off-screen render pass + charRenderer_ = std::make_unique(); + if (!charRenderer_->initialize(vkCtx_, perFrameLayout, am, renderTarget_->getRenderPass())) { LOG_ERROR("CharacterPreview: failed to initialize CharacterRenderer"); return false; } @@ -45,35 +73,187 @@ bool CharacterPreview::initialize(pipeline::AssetManager* am) { camera_->setFov(30.0f); camera_->setAspectRatio(static_cast(fboWidth_) / static_cast(fboHeight_)); // Pull camera back far enough to see full body + head with margin - // Human ~2 units tall, Tauren ~2.5. At distance 4.5 with FOV 30: - // vertical visible = 2 * 4.5 * tan(15°) ≈ 2.41 units camera_->setPosition(glm::vec3(0.0f, 4.5f, 0.9f)); camera_->setRotation(270.0f, 0.0f); - // TODO: create Vulkan offscreen render target - // createFBO(); - LOG_INFO("CharacterPreview initialized (", fboWidth_, "x", fboHeight_, ")"); return true; } void CharacterPreview::shutdown() { - // destroyFBO(); // TODO: Vulkan offscreen cleanup + // Unregister from renderer before destroying resources + auto* appRenderer = core::Application::getInstance().getRenderer(); + if (appRenderer) appRenderer->unregisterPreview(this); + if (charRenderer_) { charRenderer_->shutdown(); charRenderer_.reset(); } camera_.reset(); + destroyFBO(); modelLoaded_ = false; + compositeRendered_ = false; instanceId_ = 0; } void CharacterPreview::createFBO() { - // TODO: Create Vulkan offscreen render target for character preview + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + VmaAllocator allocator = vkCtx_->getAllocator(); + + // 1. Create off-screen render target with depth + renderTarget_ = std::make_unique(); + if (!renderTarget_->create(*vkCtx_, fboWidth_, fboHeight_, VK_FORMAT_R8G8B8A8_UNORM, true)) { + LOG_ERROR("CharacterPreview: failed to create render target"); + renderTarget_.reset(); + return; + } + + // 1b. Transition the color image from UNDEFINED to SHADER_READ_ONLY_OPTIMAL + // so that ImGui::Image doesn't sample an image in UNDEFINED layout before + // the first compositePass runs. + { + VkCommandBuffer cmd = vkCtx_->beginSingleTimeCommands(); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = renderTarget_->getColorImage(); + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier); + vkCtx_->endSingleTimeCommands(cmd); + } + + // 2. Create 1x1 dummy white texture (shadow map placeholder) + { + uint8_t white[] = {255, 255, 255, 255}; + dummyWhiteTex_ = std::make_unique(); + dummyWhiteTex_->upload(*vkCtx_, white, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + dummyWhiteTex_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); + } + + // 3. Create descriptor pool for per-frame sets (2 UBO + 2 sampler) + { + VkDescriptorPoolSize sizes[2]{}; + sizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + sizes[0].descriptorCount = MAX_FRAMES; + sizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + sizes[1].descriptorCount = MAX_FRAMES; + + VkDescriptorPoolCreateInfo ci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + ci.maxSets = MAX_FRAMES; + ci.poolSizeCount = 2; + ci.pPoolSizes = sizes; + if (vkCreateDescriptorPool(device, &ci, nullptr, &previewDescPool_) != VK_SUCCESS) { + LOG_ERROR("CharacterPreview: failed to create descriptor pool"); + return; + } + } + + // 4. Create per-frame UBOs and descriptor sets + auto* appRenderer = core::Application::getInstance().getRenderer(); + VkDescriptorSetLayout perFrameLayout = appRenderer->getPerFrameSetLayout(); + + for (uint32_t i = 0; i < MAX_FRAMES; i++) { + // Create mapped UBO + VkBufferCreateInfo bufInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufInfo.size = sizeof(GPUPerFrameData); + bufInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; + + VmaAllocationCreateInfo allocInfo{}; + allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + + VmaAllocationInfo mapInfo{}; + if (vmaCreateBuffer(allocator, &bufInfo, &allocInfo, + &previewUBO_[i], &previewUBOAlloc_[i], &mapInfo) != VK_SUCCESS) { + LOG_ERROR("CharacterPreview: failed to create UBO ", i); + return; + } + previewUBOMapped_[i] = mapInfo.pMappedData; + + // Allocate descriptor set + VkDescriptorSetAllocateInfo setAlloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + setAlloc.descriptorPool = previewDescPool_; + setAlloc.descriptorSetCount = 1; + setAlloc.pSetLayouts = &perFrameLayout; + if (vkAllocateDescriptorSets(device, &setAlloc, &previewPerFrameSet_[i]) != VK_SUCCESS) { + LOG_ERROR("CharacterPreview: failed to allocate descriptor set ", i); + return; + } + + // Write UBO binding (0) and shadow sampler binding (1) using dummy white texture + VkDescriptorBufferInfo descBuf{}; + descBuf.buffer = previewUBO_[i]; + descBuf.offset = 0; + descBuf.range = sizeof(GPUPerFrameData); + + VkDescriptorImageInfo shadowImg = dummyWhiteTex_->descriptorInfo(); + + VkWriteDescriptorSet writes[2]{}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = previewPerFrameSet_[i]; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[0].pBufferInfo = &descBuf; + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = previewPerFrameSet_[i]; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[1].pImageInfo = &shadowImg; + + vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); + } + + // 5. Register the color attachment as an ImGui texture + imguiTextureId_ = ImGui_ImplVulkan_AddTexture( + renderTarget_->getSampler(), + renderTarget_->getColorImageView(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + LOG_INFO("CharacterPreview: off-screen FBO created (", fboWidth_, "x", fboHeight_, ")"); } void CharacterPreview::destroyFBO() { - // TODO: Destroy Vulkan offscreen render target + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + VmaAllocator allocator = vkCtx_->getAllocator(); + + if (imguiTextureId_) { + ImGui_ImplVulkan_RemoveTexture(imguiTextureId_); + imguiTextureId_ = VK_NULL_HANDLE; + } + + for (uint32_t i = 0; i < MAX_FRAMES; i++) { + if (previewUBO_[i]) { + vmaDestroyBuffer(allocator, previewUBO_[i], previewUBOAlloc_[i]); + previewUBO_[i] = VK_NULL_HANDLE; + } + } + + if (previewDescPool_) { + vkDestroyDescriptorPool(device, previewDescPool_, nullptr); + previewDescPool_ = VK_NULL_HANDLE; + } + + dummyWhiteTex_.reset(); + + if (renderTarget_) { + renderTarget_->destroy(device, allocator); + renderTarget_.reset(); + } } bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, @@ -84,8 +264,11 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender, return false; } - // Remove existing instance + // Remove existing instance. + // Must wait for GPU to finish — compositePass() may have recorded draw commands + // referencing this instance's bone buffers earlier in the current frame. if (instanceId_ > 0) { + if (vkCtx_) vkDeviceWaitIdle(vkCtx_->getDevice()); charRenderer_->removeInstance(instanceId_); instanceId_ = 0; modelLoaded_ = false; @@ -592,14 +775,48 @@ void CharacterPreview::update(float deltaTime) { } void CharacterPreview::render() { - if (!charRenderer_ || !camera_ || !modelLoaded_) { + // No-op — actual rendering happens in compositePass() called from Renderer::beginFrame() +} + +void CharacterPreview::compositePass(VkCommandBuffer cmd, uint32_t frameIndex) { + // Only composite when a UI screen actually requested it this frame + if (!compositeRequested_) return; + compositeRequested_ = false; + + if (!charRenderer_ || !camera_ || !modelLoaded_ || !renderTarget_ || !renderTarget_->isValid()) { return; } - // TODO: Vulkan offscreen rendering for character preview - // Need a VkRenderTarget, begin a render pass into it, then: - // charRenderer_->render(cmd, perFrameSet, *camera_); - // For now, the preview is non-functional until Vulkan offscreen is wired up. + uint32_t fi = frameIndex % MAX_FRAMES; + + // Update per-frame UBO with preview camera matrices and studio lighting + GPUPerFrameData ubo{}; + ubo.view = camera_->getViewMatrix(); + ubo.projection = camera_->getProjectionMatrix(); + ubo.lightSpaceMatrix = glm::mat4(1.0f); + // Studio lighting: key light from upper-right-front + ubo.lightDir = glm::vec4(glm::normalize(glm::vec3(0.5f, -0.7f, 0.5f)), 0.0f); + ubo.lightColor = glm::vec4(1.0f, 0.95f, 0.9f, 0.0f); + ubo.ambientColor = glm::vec4(0.35f, 0.35f, 0.4f, 0.0f); + ubo.viewPos = glm::vec4(camera_->getPosition(), 0.0f); + // No fog in preview + ubo.fogColor = glm::vec4(0.05f, 0.05f, 0.1f, 0.0f); + ubo.fogParams = glm::vec4(9999.0f, 10000.0f, 0.0f, 0.0f); + // Shadows disabled + ubo.shadowParams = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f); + + std::memcpy(previewUBOMapped_[fi], &ubo, sizeof(GPUPerFrameData)); + + // Begin off-screen render pass + VkClearColorValue clearColor = {{0.05f, 0.05f, 0.1f, 1.0f}}; + renderTarget_->beginPass(cmd, clearColor); + + // Render the character model + charRenderer_->render(cmd, previewPerFrameSet_[fi], *camera_); + + renderTarget_->endPass(cmd); + + compositeRendered_ = true; } void CharacterPreview::rotate(float yawDelta) { diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 635cecbb..6a507d82 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -86,12 +86,14 @@ CharacterRenderer::~CharacterRenderer() { } bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, - pipeline::AssetManager* am) { + pipeline::AssetManager* am, + VkRenderPass renderPassOverride) { core::Logger::getInstance().info("Initializing character renderer (Vulkan)..."); vkCtx_ = ctx; assetManager = am; perFrameLayout_ = perFrameLayout; + renderPassOverride_ = renderPassOverride; VkDevice device = vkCtx_->getDevice(); @@ -182,7 +184,8 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram return false; } - VkRenderPass mainPass = vkCtx_->getImGuiRenderPass(); + VkRenderPass mainPass = renderPassOverride_ ? renderPassOverride_ : vkCtx_->getImGuiRenderPass(); + VkSampleCountFlagBits samples = renderPassOverride_ ? VK_SAMPLE_COUNT_1_BIT : vkCtx_->getMsaaSamples(); // --- Vertex input --- // M2Vertex: vec3 pos(12) + uint8[4] boneWeights(4) + uint8[4] boneIndices(4) + @@ -210,7 +213,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blendState) - .setMultisample(vkCtx_->getMsaaSamples()) + .setMultisample(samples) .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -252,6 +255,9 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram void CharacterRenderer::shutdown() { if (!vkCtx_) return; + LOG_INFO("CharacterRenderer::shutdown instances=", instances.size(), + " models=", models.size(), " override=", (void*)renderPassOverride_); + vkDeviceWaitIdle(vkCtx_->getDevice()); VkDevice device = vkCtx_->getDevice(); VmaAllocator alloc = vkCtx_->getAllocator(); @@ -1321,6 +1327,13 @@ glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, floa // --- Rendering --- void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) { + // Periodic instance count log (every ~10s at 30fps) + if (!renderPassOverride_) { + renderLogCounter_++; + if (renderLogCounter_ % 300 == 1) { + LOG_INFO("CharRenderer[WORLD]::render instances=", instances.size()); + } + } if (instances.empty() || !opaquePipeline_) { return; } @@ -2196,6 +2209,9 @@ void CharacterRenderer::clearTextureSlotOverride(uint32_t instanceId, uint16_t t void CharacterRenderer::setInstanceVisible(uint32_t instanceId, bool visible) { auto it = instances.find(instanceId); if (it != instances.end()) { + if (it->second.visible != visible) { + LOG_INFO("CharacterRenderer::setInstanceVisible id=", instanceId, " visible=", visible); + } it->second.visible = visible; // Also hide/show attached weapons (for first-person mode) @@ -2212,6 +2228,11 @@ void CharacterRenderer::removeInstance(uint32_t instanceId) { auto it = instances.find(instanceId); if (it == instances.end()) return; + LOG_INFO("CharacterRenderer::removeInstance id=", instanceId, + " pos=(", it->second.position.x, ",", it->second.position.y, ",", it->second.position.z, ")", + " remaining=", instances.size() - 1, + " override=", (void*)renderPassOverride_); + // Remove child attachments first (helmets/weapons), otherwise they leak as // orphan render instances when the parent creature despawns. auto attachments = it->second.weaponAttachments; @@ -2585,7 +2606,8 @@ void CharacterRenderer::recreatePipelines() { return; } - VkRenderPass mainPass = vkCtx_->getImGuiRenderPass(); + VkRenderPass mainPass = renderPassOverride_ ? renderPassOverride_ : vkCtx_->getImGuiRenderPass(); + VkSampleCountFlagBits samples = renderPassOverride_ ? VK_SAMPLE_COUNT_1_BIT : vkCtx_->getMsaaSamples(); // --- Vertex input --- VkVertexInputBindingDescription charBinding{}; @@ -2610,13 +2632,17 @@ void CharacterRenderer::recreatePipelines() { .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blendState) - .setMultisample(vkCtx_->getMsaaSamples()) + .setMultisample(samples) .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) .build(device); }; + LOG_INFO("CharacterRenderer::recreatePipelines: renderPass=", (void*)mainPass, + " samples=", static_cast(samples), + " pipelineLayout=", (void*)pipelineLayout_); + opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), true); alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); @@ -2625,7 +2651,16 @@ void CharacterRenderer::recreatePipelines() { charVert.destroy(); charFrag.destroy(); - core::Logger::getInstance().info("CharacterRenderer: pipelines recreated"); + if (!opaquePipeline_ || !alphaTestPipeline_ || !alphaPipeline_ || !additivePipeline_) { + LOG_ERROR("CharacterRenderer::recreatePipelines FAILED: opaque=", (void*)opaquePipeline_, + " alphaTest=", (void*)alphaTestPipeline_, + " alpha=", (void*)alphaPipeline_, + " additive=", (void*)additivePipeline_, + " renderPass=", (void*)mainPass, " samples=", static_cast(samples)); + } else { + LOG_INFO("CharacterRenderer: pipelines recreated successfully (samples=", + static_cast(samples), ")"); + } } } // namespace rendering diff --git a/src/rendering/frustum.cpp b/src/rendering/frustum.cpp index 6d0d58d4..45e9ff60 100644 --- a/src/rendering/frustum.cpp +++ b/src/rendering/frustum.cpp @@ -5,45 +5,56 @@ namespace wowee { namespace rendering { void Frustum::extractFromMatrix(const glm::mat4& vp) { - // Extract planes from view-projection matrix - // Based on Gribb & Hartmann method (Fast Extraction of Viewing Frustum Planes) + // Extract frustum planes from view-projection matrix. + // Vulkan clip-space conventions (GLM_FORCE_DEPTH_ZERO_TO_ONE + Y-flip): + // x_clip ∈ [-w, w], y_clip ∈ [-w, w] (Y flipped in proj), z_clip ∈ [0, w] + // + // Gribb & Hartmann method adapted for Vulkan depth [0,1]. + // Left/Right/Top/Bottom use the standard row4 ± row1/row2 formulas + // (the Y-flip swaps the TOP/BOTTOM row2 sign, but the extracted half-spaces + // are still correct — they just get each other's label. We swap the + // assignments so the enum names match the geometric meaning.) - // Left plane: row4 + row1 + // Left plane: row4 + row1 (x_clip >= -w_clip) planes[LEFT].normal.x = vp[0][3] + vp[0][0]; planes[LEFT].normal.y = vp[1][3] + vp[1][0]; planes[LEFT].normal.z = vp[2][3] + vp[2][0]; planes[LEFT].distance = vp[3][3] + vp[3][0]; normalizePlane(planes[LEFT]); - // Right plane: row4 - row1 + // Right plane: row4 - row1 (x_clip <= w_clip) planes[RIGHT].normal.x = vp[0][3] - vp[0][0]; planes[RIGHT].normal.y = vp[1][3] - vp[1][0]; planes[RIGHT].normal.z = vp[2][3] - vp[2][0]; planes[RIGHT].distance = vp[3][3] - vp[3][0]; normalizePlane(planes[RIGHT]); - // Bottom plane: row4 + row2 - planes[BOTTOM].normal.x = vp[0][3] + vp[0][1]; - planes[BOTTOM].normal.y = vp[1][3] + vp[1][1]; - planes[BOTTOM].normal.z = vp[2][3] + vp[2][1]; - planes[BOTTOM].distance = vp[3][3] + vp[3][1]; - normalizePlane(planes[BOTTOM]); + // With the Vulkan Y-flip (proj[1][1] negated), row4+row2 extracts + // what is geometrically the TOP plane and row4-row2 extracts BOTTOM. + // Swap the assignments so enum labels match geometry. - // Top plane: row4 - row2 - planes[TOP].normal.x = vp[0][3] - vp[0][1]; - planes[TOP].normal.y = vp[1][3] - vp[1][1]; - planes[TOP].normal.z = vp[2][3] - vp[2][1]; - planes[TOP].distance = vp[3][3] - vp[3][1]; + // Top plane (geometric): row4 - row2 after Y-flip + planes[TOP].normal.x = vp[0][3] + vp[0][1]; + planes[TOP].normal.y = vp[1][3] + vp[1][1]; + planes[TOP].normal.z = vp[2][3] + vp[2][1]; + planes[TOP].distance = vp[3][3] + vp[3][1]; normalizePlane(planes[TOP]); - // Near plane: row4 + row3 - planes[NEAR].normal.x = vp[0][3] + vp[0][2]; - planes[NEAR].normal.y = vp[1][3] + vp[1][2]; - planes[NEAR].normal.z = vp[2][3] + vp[2][2]; - planes[NEAR].distance = vp[3][3] + vp[3][2]; + // Bottom plane (geometric): row4 + row2 after Y-flip + planes[BOTTOM].normal.x = vp[0][3] - vp[0][1]; + planes[BOTTOM].normal.y = vp[1][3] - vp[1][1]; + planes[BOTTOM].normal.z = vp[2][3] - vp[2][1]; + planes[BOTTOM].distance = vp[3][3] - vp[3][1]; + normalizePlane(planes[BOTTOM]); + + // Near plane: row3 (z_clip >= 0 in Vulkan depth [0,1]) + planes[NEAR].normal.x = vp[0][2]; + planes[NEAR].normal.y = vp[1][2]; + planes[NEAR].normal.z = vp[2][2]; + planes[NEAR].distance = vp[3][2]; normalizePlane(planes[NEAR]); - // Far plane: row4 - row3 + // Far plane: row4 - row3 (z_clip <= w_clip) planes[FAR].normal.x = vp[0][3] - vp[0][2]; planes[FAR].normal.y = vp[1][3] - vp[1][2]; planes[FAR].normal.z = vp[2][3] - vp[2][2]; diff --git a/src/rendering/performance_hud.cpp b/src/rendering/performance_hud.cpp index f801fab0..d939f4f9 100644 --- a/src/rendering/performance_hud.cpp +++ b/src/rendering/performance_hud.cpp @@ -412,32 +412,37 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) { if (showControls) { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "CONTROLS"); ImGui::Separator(); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F1: Toggle HUD"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F2: Wireframe"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F3: Single tile"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F4: Culling"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F5: Stats"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F6: Multi-tile"); + + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Movement"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "WASD: Move/Strafe"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Q/E: Turn left/right"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Space: Jump"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "X: Sit/Stand"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "~: Auto-run"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Z: Sheathe weapons"); + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "UI Panels"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "B: Bags/Inventory"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "C: Character sheet"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "L: Quest log"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "N: Talents"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "P: Spellbook"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "M: World map"); + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Combat & Chat"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "1-0,-,=: Action bar"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Tab: Target cycle"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Enter: Chat"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "/: Chat command"); + + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Debug"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F1: Toggle this HUD"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F4: Toggle shadows"); ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F7: Level-up FX"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F8: Water"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F9: Time"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F10: Sun/Moon"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F11: Stars"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F12: Fog"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "+/-: Change time"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "C: Clouds"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "[/]: Density"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "L: Lens Flare"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ",/.: Intensity"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "M: Moon Cycle"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ";/': Moon Phase"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "W: Weather"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ": Wx Intensity"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "K: Spawn Character"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "J: Remove Chars"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "O: Spawn Test WMO"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Shift+O: Real WMO"); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "P: Clear WMOs"); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Esc: Settings/Close"); } ImGui::End(); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 6f91076a..78034f94 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -19,6 +19,7 @@ #include "rendering/charge_effect.hpp" #include "rendering/levelup_effect.hpp" #include "rendering/character_renderer.hpp" +#include "rendering/character_preview.hpp" #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" #include "rendering/minimap.hpp" @@ -723,6 +724,21 @@ void Renderer::shutdown() { LOG_INFO("Renderer shutdown"); } +void Renderer::registerPreview(CharacterPreview* preview) { + if (!preview) return; + auto it = std::find(activePreviews_.begin(), activePreviews_.end(), preview); + if (it == activePreviews_.end()) { + activePreviews_.push_back(preview); + } +} + +void Renderer::unregisterPreview(CharacterPreview* preview) { + auto it = std::find(activePreviews_.begin(), activePreviews_.end(), preview); + if (it != activePreviews_.end()) { + activePreviews_.erase(it); + } +} + void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) { if (!vkCtx) return; @@ -840,6 +856,13 @@ void Renderer::beginFrame() { worldMap->compositePass(currentCmd); } + // Character preview composite passes + for (auto* preview : activePreviews_) { + if (preview && preview->isModelLoaded()) { + preview->compositePass(currentCmd, vkCtx->getCurrentFrame()); + } + } + // Shadow pre-pass (before main render pass) if (shadowsEnabled && shadowDepthImage != VK_NULL_HANDLE) { renderShadowPass(); @@ -3007,6 +3030,15 @@ void Renderer::renderOverlay(const glm::vec4& color) { void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { (void)world; + { + static int rwLogCounter = 0; + if (++rwLogCounter % 300 == 1) { + LOG_INFO("Renderer::renderWorld frame=", rwLogCounter, + " cmd=", (void*)currentCmd, + " charRenderer=", (void*)characterRenderer.get()); + } + } + auto renderStart = std::chrono::steady_clock::now(); lastTerrainRenderMs = 0.0; lastWMORenderMs = 0.0; diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp index 8264ced2..9ebec4f4 100644 --- a/src/rendering/vk_context.cpp +++ b/src/rendering/vk_context.cpp @@ -1041,8 +1041,18 @@ bool VkContext::recreateSwapchain(int width, int height) { VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) { auto& frame = frames[currentFrame]; - // Wait for this frame's fence - vkWaitForFences(device, 1, &frame.inFlightFence, VK_TRUE, UINT64_MAX); + // Wait for this frame's fence (with timeout to detect GPU hangs) + static int beginFrameCounter = 0; + beginFrameCounter++; + VkResult fenceResult = vkWaitForFences(device, 1, &frame.inFlightFence, VK_TRUE, 5000000000ULL); // 5 second timeout + if (fenceResult == VK_TIMEOUT) { + LOG_ERROR("beginFrame[", beginFrameCounter, "] FENCE TIMEOUT (5s) on frame slot ", currentFrame, " — GPU hang detected!"); + return VK_NULL_HANDLE; + } + if (fenceResult != VK_SUCCESS) { + LOG_ERROR("beginFrame[", beginFrameCounter, "] fence wait failed: ", (int)fenceResult); + return VK_NULL_HANDLE; + } // Acquire next swapchain image VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, @@ -1070,7 +1080,13 @@ VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) { } void VkContext::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) { - vkEndCommandBuffer(cmd); + static int endFrameCounter = 0; + endFrameCounter++; + + VkResult endResult = vkEndCommandBuffer(cmd); + if (endResult != VK_SUCCESS) { + LOG_ERROR("endFrame[", endFrameCounter, "] vkEndCommandBuffer FAILED: ", (int)endResult); + } auto& frame = frames[currentFrame]; @@ -1086,8 +1102,9 @@ void VkContext::endFrame(VkCommandBuffer cmd, uint32_t imageIndex) { submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = &frame.renderFinishedSemaphore; - if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, frame.inFlightFence) != VK_SUCCESS) { - LOG_ERROR("Failed to submit draw command buffer"); + VkResult submitResult = vkQueueSubmit(graphicsQueue, 1, &submitInfo, frame.inFlightFence); + if (submitResult != VK_SUCCESS) { + LOG_ERROR("endFrame[", endFrameCounter, "] vkQueueSubmit FAILED: ", (int)submitResult); } VkPresentInfoKHR presentInfo{}; diff --git a/src/rendering/vk_render_target.cpp b/src/rendering/vk_render_target.cpp index d677bce7..338133e0 100644 --- a/src/rendering/vk_render_target.cpp +++ b/src/rendering/vk_render_target.cpp @@ -9,9 +9,10 @@ VkRenderTarget::~VkRenderTarget() { // Must call destroy() explicitly with device/allocator before destruction } -bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkFormat format) { +bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkFormat format, bool withDepth) { VkDevice device = ctx.getDevice(); VmaAllocator allocator = ctx.getAllocator(); + hasDepth_ = withDepth; // Create color image (COLOR_ATTACHMENT + SAMPLED for reading in subsequent passes) colorImage_ = createImage(device, allocator, width, height, format, @@ -22,6 +23,17 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF return false; } + // Create depth image if requested + if (withDepth) { + depthImage_ = createImage(device, allocator, width, height, + VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + if (!depthImage_.image) { + LOG_ERROR("VkRenderTarget: failed to create depth image (", width, "x", height, ")"); + destroy(device, allocator); + return false; + } + } + // Create sampler (linear filtering, clamp to edge) VkSamplerCreateInfo samplerInfo{}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; @@ -41,44 +53,77 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF } // Create render pass - // Color attachment: UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL (during pass) - // -> SHADER_READ_ONLY_OPTIMAL (final layout, ready for sampling) - VkAttachmentDescription colorAttachment{}; - colorAttachment.format = format; - colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; - colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - colorAttachment.finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + VkAttachmentDescription attachments[2]{}; + // Color attachment: UNDEFINED -> COLOR_ATTACHMENT_OPTIMAL -> SHADER_READ_ONLY_OPTIMAL + attachments[0].format = format; + attachments[0].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + // Depth attachment (only used when withDepth) + attachments[1].format = VK_FORMAT_D32_SFLOAT; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachments[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachments[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkAttachmentReference colorRef{}; colorRef.attachment = 0; colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + VkAttachmentReference depthRef{}; + depthRef.attachment = 1; + depthRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorRef; + if (withDepth) subpass.pDepthStencilAttachment = &depthRef; - // Dependency: external -> subpass 0 (wait for previous reads to finish) - VkSubpassDependency dependency{}; - dependency.srcSubpass = VK_SUBPASS_EXTERNAL; - dependency.dstSubpass = 0; - dependency.srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; - dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; - dependency.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; - dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + // Dependencies + VkSubpassDependency dependencies[2]{}; + uint32_t depCount = 1; + + // Input dependency: wait for previous fragment shader reads before writing + dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL; + dependencies[0].dstSubpass = 0; + dependencies[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dependencies[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependencies[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + if (withDepth) { + dependencies[0].dstStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; + dependencies[0].dstAccessMask |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + // Output dependency (depth targets only): ensure writes complete before fragment reads + dependencies[1].srcSubpass = 0; + dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL; + dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + dependencies[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + dependencies[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + depCount = 2; + } VkRenderPassCreateInfo rpInfo{}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; - rpInfo.attachmentCount = 1; - rpInfo.pAttachments = &colorAttachment; + rpInfo.attachmentCount = withDepth ? 2u : 1u; + rpInfo.pAttachments = attachments; rpInfo.subpassCount = 1; rpInfo.pSubpasses = &subpass; - rpInfo.dependencyCount = 1; - rpInfo.pDependencies = &dependency; + rpInfo.dependencyCount = depCount; + rpInfo.pDependencies = dependencies; if (vkCreateRenderPass(device, &rpInfo, nullptr, &renderPass_) != VK_SUCCESS) { LOG_ERROR("VkRenderTarget: failed to create render pass"); @@ -87,11 +132,12 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF } // Create framebuffer + VkImageView fbAttachments[2] = { colorImage_.imageView, depthImage_.imageView }; VkFramebufferCreateInfo fbInfo{}; fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; fbInfo.renderPass = renderPass_; - fbInfo.attachmentCount = 1; - fbInfo.pAttachments = &colorImage_.imageView; + fbInfo.attachmentCount = withDepth ? 2u : 1u; + fbInfo.pAttachments = fbAttachments; fbInfo.width = width; fbInfo.height = height; fbInfo.layers = 1; @@ -102,7 +148,7 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height, VkF return false; } - LOG_INFO("VkRenderTarget created (", width, "x", height, ")"); + LOG_INFO("VkRenderTarget created (", width, "x", height, withDepth ? ", with depth)" : ")"); return true; } @@ -119,7 +165,9 @@ void VkRenderTarget::destroy(VkDevice device, VmaAllocator allocator) { vkDestroySampler(device, sampler_, nullptr); sampler_ = VK_NULL_HANDLE; } + destroyImage(device, allocator, depthImage_); destroyImage(device, allocator, colorImage_); + hasDepth_ = false; } void VkRenderTarget::beginPass(VkCommandBuffer cmd, const VkClearColorValue& clear) { @@ -130,10 +178,11 @@ void VkRenderTarget::beginPass(VkCommandBuffer cmd, const VkClearColorValue& cle rpBegin.renderArea.offset = {0, 0}; rpBegin.renderArea.extent = getExtent(); - VkClearValue clearValue{}; - clearValue.color = clear; - rpBegin.clearValueCount = 1; - rpBegin.pClearValues = &clearValue; + VkClearValue clearValues[2]{}; + clearValues[0].color = clear; + clearValues[1].depthStencil = {1.0f, 0}; + rpBegin.clearValueCount = hasDepth_ ? 2u : 1u; + rpBegin.pClearValues = clearValues; vkCmdBeginRenderPass(cmd, &rpBegin, VK_SUBPASS_CONTENTS_INLINE); diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index 0fd00b9d..fa81756f 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -1,5 +1,7 @@ #include "ui/character_create_screen.hpp" #include "rendering/character_preview.hpp" +#include "rendering/renderer.hpp" +#include "core/application.hpp" #include "game/game_handler.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_layout.hpp" @@ -128,7 +130,10 @@ void CharacterCreateScreen::initializePreview(pipeline::AssetManager* am) { assetManager_ = am; if (!preview_) { preview_ = std::make_unique(); - preview_->initialize(am); + if (preview_->initialize(am)) { + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) renderer->registerPreview(preview_.get()); + } } // Force model reload prevRaceIndex_ = -1; @@ -332,6 +337,7 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { if (preview_) { updatePreviewIfNeeded(); preview_->render(); + preview_->requestComposite(); } ImVec2 displaySize = ImGui::GetIO().DisplaySize; @@ -363,13 +369,10 @@ void CharacterCreateScreen::render(game::GameHandler& /*gameHandler*/) { static_cast(preview_->getHeight())); } - // TODO: Vulkan offscreen preview render target if (preview_->getTextureId()) { ImGui::Image( - static_cast(0), - ImVec2(imgW, imgH), - ImVec2(0.0f, 1.0f), - ImVec2(1.0f, 0.0f)); + reinterpret_cast(preview_->getTextureId()), + ImVec2(imgW, imgH)); } // Mouse drag rotation on the preview image diff --git a/src/ui/character_screen.cpp b/src/ui/character_screen.cpp index 7f661c40..406164ac 100644 --- a/src/ui/character_screen.cpp +++ b/src/ui/character_screen.cpp @@ -1,5 +1,6 @@ #include "ui/character_screen.hpp" #include "rendering/character_preview.hpp" +#include "rendering/renderer.hpp" #include "pipeline/asset_manager.hpp" #include "core/application.hpp" #include "core/logger.hpp" @@ -250,6 +251,9 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { if (!previewInitialized_) { LOG_WARNING("CharacterScreen: failed to init CharacterPreview"); preview_.reset(); + } else { + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) renderer->registerPreview(preview_.get()); } } if (preview_) { @@ -280,9 +284,10 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { previewEquipHash_ = equipHash; } - // Drive preview animation and render to its FBO. + // Drive preview animation and request composite for next beginFrame. preview_->update(ImGui::GetIO().DeltaTime); preview_->render(); + preview_->requestComposite(); } } @@ -290,7 +295,7 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { ImGui::BeginChild("CharDetails", ImVec2(detailPanelW, listH), true); // 3D preview portrait - if (preview_ && preview_->getTextureId() != 0) { + if (preview_ && preview_->getTextureId()) { float imgW = ImGui::GetContentRegionAvail().x; float imgH = imgW * (static_cast(preview_->getHeight()) / static_cast(preview_->getWidth())); @@ -301,14 +306,9 @@ void CharacterScreen::render(game::GameHandler& gameHandler) { imgW = imgH * (static_cast(preview_->getWidth()) / static_cast(preview_->getHeight())); } - // TODO: Vulkan offscreen preview render target - if (preview_->getTextureId()) { - ImGui::Image( - static_cast(0), - ImVec2(imgW, imgH), - ImVec2(0.0f, 1.0f), - ImVec2(1.0f, 0.0f)); - } + ImGui::Image( + reinterpret_cast(preview_->getTextureId()), + ImVec2(imgW, imgH)); if (ImGui::IsItemHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { preview_->rotate(ImGui::GetIO().MouseDelta.x * 0.2f); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index 56bdc3ec..aa48cc40 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -5,6 +5,7 @@ #include "core/input.hpp" #include "rendering/character_preview.hpp" #include "rendering/character_renderer.hpp" +#include "rendering/renderer.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" #include "pipeline/blp_loader.hpp" @@ -175,6 +176,8 @@ void InventoryScreen::initPreview() { charPreview_.reset(); return; } + auto* renderer = core::Application::getInstance().getRenderer(); + if (renderer) renderer->registerPreview(charPreview_.get()); } charPreview_->loadCharacter(playerRace_, playerGender_, @@ -925,6 +928,7 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) { if (charPreview_ && previewInitialized_) { charPreview_->update(ImGui::GetIO().DeltaTime); charPreview_->render(); + charPreview_->requestComposite(); } ImGui::SetNextWindowPos(ImVec2(20.0f, 80.0f), ImGuiCond_FirstUseEver); @@ -1120,9 +1124,8 @@ void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) { // Background for preview area drawList->AddRectFilled(pMin, pMax, IM_COL32(13, 13, 25, 255)); drawList->AddImage( - (ImTextureID)(uintptr_t)charPreview_->getTextureId(), - pMin, pMax, - ImVec2(0, 1), ImVec2(1, 0)); // flip Y for GL + reinterpret_cast(charPreview_->getTextureId()), + pMin, pMax); drawList->AddRect(pMin, pMax, IM_COL32(60, 60, 80, 200)); // Drag-to-rotate: detect mouse drag over the preview image