diff --git a/src/core/texture_cache.cpp b/src/core/texture_cache.cpp index 5eac288..9ee92bc 100644 --- a/src/core/texture_cache.cpp +++ b/src/core/texture_cache.cpp @@ -122,6 +122,19 @@ void TextureCache::watchBinding(TextureHandle handle, VkDescriptorSet set, uint3 // Back-reference for fast per-set markUsed _setToHandles[set].push_back(handle); + + // If the texture is already resident, immediately patch the new descriptor + // so re-spawned models using cached textures get the correct bindings. + if (e.state == EntryState::Resident && e.image.imageView != VK_NULL_HANDLE && set != VK_NULL_HANDLE) + { + if (!_context || !_context->getDevice()) return; + DescriptorWriter writer; + writer.write_image(static_cast(binding), e.image.imageView, + p.sampler ? p.sampler : e.sampler, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.update_set(_context->getDevice()->device(), set); + } } void TextureCache::unwatchSet(VkDescriptorSet set) diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 428f5a9..3a901d8 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -254,13 +254,6 @@ void VulkanEngine::init() auto imguiPass = std::make_unique(); _renderPassManager->setImGuiPass(std::move(imguiPass)); - const std::string structurePath = _assetManager->modelPath("mirage2000/scene.gltf"); - const auto structureFile = _assetManager->loadGLTF(structurePath); - - assert(structureFile.has_value()); - - _sceneManager->loadScene("structure", *structureFile); - _resourceManager->set_deferred_uploads(true); //everything went fine @@ -330,6 +323,8 @@ void VulkanEngine::init_default_data() BoundsType::Sphere); } + addGLTFInstance("mirage", "mirage2000/scene.gltf", glm::mat4(1.0f)); + _mainDeletionQueue.push_function([&]() { _resourceManager->destroy_image(_whiteImage); _resourceManager->destroy_image(_greyImage); @@ -340,6 +335,32 @@ void VulkanEngine::init_default_data() //< default_img } +bool VulkanEngine::addGLTFInstance(const std::string &instanceName, + const std::string &modelRelativePath, + const glm::mat4 &transform) +{ + if (!_assetManager || !_sceneManager) + { + return false; + } + + const std::string fullPath = _assetManager->modelPath(modelRelativePath); + auto gltf = _assetManager->loadGLTF(fullPath); + if (!gltf.has_value() || !gltf.value()) + { + return false; + } + + // Provide a readable debug name for UI/picking when missing. + if ((*gltf)->debugName.empty()) + { + (*gltf)->debugName = modelRelativePath; + } + + _sceneManager->addGLTFInstance(instanceName, *gltf, transform); + return true; +} + void VulkanEngine::cleanup() { vkDeviceWaitIdle(_deviceManager->device()); @@ -449,6 +470,9 @@ void VulkanEngine::draw() { _hoverPick.mesh = hoverObj.sourceMesh; _hoverPick.scene = hoverObj.sourceScene; + _hoverPick.node = hoverObj.sourceNode; + _hoverPick.ownerType = hoverObj.ownerType; + _hoverPick.ownerName = hoverObj.ownerName; _hoverPick.worldPos = hoverPos; _hoverPick.worldTransform = hoverObj.transform; _hoverPick.firstIndex = hoverObj.firstIndex; @@ -459,6 +483,8 @@ void VulkanEngine::draw() else { _hoverPick.valid = false; + _hoverPick.ownerName.clear(); + _hoverPick.ownerType = RenderObject::OwnerType::None; } } @@ -779,6 +805,9 @@ void VulkanEngine::run() { _lastPick.mesh = hitObject.sourceMesh; _lastPick.scene = hitObject.sourceScene; + _lastPick.node = hitObject.sourceNode; + _lastPick.ownerType = hitObject.ownerType; + _lastPick.ownerName = hitObject.ownerName; _lastPick.worldPos = hitPos; _lastPick.worldTransform = hitObject.transform; _lastPick.firstIndex = hitObject.firstIndex; @@ -790,6 +819,8 @@ void VulkanEngine::run() else { _lastPick.valid = false; + _lastPick.ownerName.clear(); + _lastPick.ownerType = RenderObject::OwnerType::None; _lastPickObjectID = 0; } } @@ -809,10 +840,13 @@ void VulkanEngine::run() PickInfo info{}; info.mesh = obj.sourceMesh; info.scene = obj.sourceScene; + info.node = obj.sourceNode; + info.ownerType = obj.ownerType; + info.ownerName = obj.ownerName; // Use bounds origin transformed to world as a representative point. glm::vec3 centerWorld = glm::vec3(obj.transform * glm::vec4(obj.bounds.origin, 1.0f)); info.worldPos = centerWorld; - info.worldTransform = obj.transform; + info.worldTransform = obj.transform; info.firstIndex = obj.firstIndex; info.indexCount = obj.indexCount; info.surfaceIndex = obj.surfaceIndex; @@ -856,6 +890,8 @@ void VulkanEngine::run() { // No object under cursor in ID buffer: clear last pick. _lastPick.valid = false; + _lastPick.ownerName.clear(); + _lastPick.ownerType = RenderObject::OwnerType::None; _lastPickObjectID = 0; } else @@ -868,6 +904,9 @@ void VulkanEngine::run() glm::vec3 fallbackPos = glm::vec3(picked.transform[3]); _lastPick.mesh = picked.sourceMesh; _lastPick.scene = picked.sourceScene; + _lastPick.node = picked.sourceNode; + _lastPick.ownerType = picked.ownerType; + _lastPick.ownerName = picked.ownerName; _lastPick.worldPos = fallbackPos; _lastPick.worldTransform = picked.transform; _lastPick.firstIndex = picked.firstIndex; @@ -878,6 +917,8 @@ void VulkanEngine::run() else { _lastPick.valid = false; + _lastPick.ownerName.clear(); + _lastPick.ownerType = RenderObject::OwnerType::None; _lastPickObjectID = 0; } } @@ -968,6 +1009,7 @@ void MeshNode::Draw(const glm::mat4 &topMatrix, DrawContext &ctx) def.surfaceIndex = i; def.objectID = ctx.nextID++; def.sourceScene = scene; + def.sourceNode = this; if (s.material->data.passType == MaterialPass::Transparent) { diff --git a/src/core/vk_engine.h b/src/core/vk_engine.h index 76a9746..bf17f4b 100644 --- a/src/core/vk_engine.h +++ b/src/core/vk_engine.h @@ -120,6 +120,9 @@ public: { MeshAsset *mesh = nullptr; LoadedGLTF *scene = nullptr; + Node *node = nullptr; + RenderObject::OwnerType ownerType = RenderObject::OwnerType::None; + std::string ownerName; glm::vec3 worldPos{0.0f}; glm::mat4 worldTransform{1.0f}; uint32_t indexCount = 0; @@ -174,6 +177,12 @@ public: // Query a conservative streaming texture budget for the texture cache. size_t query_texture_budget_bytes() const; + // Convenience helper: load a glTF from assets/models and add it as a runtime instance. + // modelRelativePath is relative to the AssetManager model root. + bool addGLTFInstance(const std::string &instanceName, + const std::string &modelRelativePath, + const glm::mat4 &transform = glm::mat4(1.f)); + bool resize_requested{false}; bool freeze_rendering{false}; diff --git a/src/core/vk_engine_ui.cpp b/src/core/vk_engine_ui.cpp index 54b8e38..71213dd 100644 --- a/src/core/vk_engine_ui.cpp +++ b/src/core/vk_engine_ui.cpp @@ -7,11 +7,13 @@ #include "vk_engine.h" #include "imgui.h" +#include "ImGuizmo.h" #include "render/primitives.h" #include "vk_mem_alloc.h" #include "render/vk_renderpass_tonemap.h" #include "render/vk_renderpass_background.h" +#include #include "render/rg_graph.h" #include "core/vk_pipeline_manager.h" #include "core/texture_cache.h" @@ -21,597 +23,757 @@ #include "mesh_bvh.h" -namespace { - -// Background / compute playground -static void ui_background(VulkanEngine *eng) +namespace { - if (!eng || !eng->_renderPassManager) return; - auto *background_pass = eng->_renderPassManager->getPass(); - if (!background_pass) + // Background / compute playground + static void ui_background(VulkanEngine *eng) { - ImGui::TextUnformatted("Background pass not available"); - return; + if (!eng || !eng->_renderPassManager) return; + auto *background_pass = eng->_renderPassManager->getPass(); + if (!background_pass) + { + ImGui::TextUnformatted("Background pass not available"); + return; + } + + ComputeEffect &selected = background_pass->_backgroundEffects[background_pass->_currentEffect]; + + ImGui::Text("Selected effect: %s", selected.name); + ImGui::SliderInt("Effect Index", &background_pass->_currentEffect, 0, + (int) background_pass->_backgroundEffects.size() - 1); + ImGui::InputFloat4("data1", reinterpret_cast(&selected.data.data1)); + ImGui::InputFloat4("data2", reinterpret_cast(&selected.data.data2)); + ImGui::InputFloat4("data3", reinterpret_cast(&selected.data.data3)); + ImGui::InputFloat4("data4", reinterpret_cast(&selected.data.data4)); + + ImGui::Separator(); + ImGui::SliderFloat("Render Scale", &eng->renderScale, 0.3f, 1.f); } - ComputeEffect &selected = background_pass->_backgroundEffects[background_pass->_currentEffect]; - - ImGui::Text("Selected effect: %s", selected.name); - ImGui::SliderInt("Effect Index", &background_pass->_currentEffect, 0, - (int) background_pass->_backgroundEffects.size() - 1); - ImGui::InputFloat4("data1", reinterpret_cast(&selected.data.data1)); - ImGui::InputFloat4("data2", reinterpret_cast(&selected.data.data2)); - ImGui::InputFloat4("data3", reinterpret_cast(&selected.data.data3)); - ImGui::InputFloat4("data4", reinterpret_cast(&selected.data.data4)); - - ImGui::Separator(); - ImGui::SliderFloat("Render Scale", &eng->renderScale, 0.3f, 1.f); -} - -// IBL test grid spawner (spheres varying metallic/roughness) -static void spawn_ibl_test(VulkanEngine *eng) -{ - if (!eng || !eng->_assetManager || !eng->_sceneManager) return; - using MC = GLTFMetallic_Roughness::MaterialConstants; - - std::vector verts; - std::vector inds; - primitives::buildSphere(verts, inds, 24, 24); - - const float mVals[5] = {0.0f, 0.25f, 0.5f, 0.75f, 1.0f}; - const float rVals[5] = {0.04f, 0.25f, 0.5f, 0.75f, 1.0f}; - const float spacing = 1.6f; - const glm::vec3 origin(-spacing * 2.0f, 0.0f, -spacing * 2.0f); - - for (int iy = 0; iy < 5; ++iy) + // IBL test grid spawner (spheres varying metallic/roughness) + static void spawn_ibl_test(VulkanEngine *eng) { - for (int ix = 0; ix < 5; ++ix) - { - MC c{}; - c.colorFactors = glm::vec4(0.82f, 0.82f, 0.82f, 1.0f); - c.metal_rough_factors = glm::vec4(mVals[ix], rVals[iy], 0.0f, 0.0f); - const std::string base = fmt::format("ibltest.m{}_r{}", ix, iy); - auto mat = eng->_assetManager->createMaterialFromConstants(base + ".mat", c, MaterialPass::MainColor); + if (!eng || !eng->_assetManager || !eng->_sceneManager) return; + using MC = GLTFMetallic_Roughness::MaterialConstants; - auto mesh = eng->_assetManager->createMesh(base + ".mesh", + std::vector verts; + std::vector inds; + primitives::buildSphere(verts, inds, 24, 24); + + const float mVals[5] = {0.0f, 0.25f, 0.5f, 0.75f, 1.0f}; + const float rVals[5] = {0.04f, 0.25f, 0.5f, 0.75f, 1.0f}; + const float spacing = 1.6f; + const glm::vec3 origin(-spacing * 2.0f, 0.0f, -spacing * 2.0f); + + for (int iy = 0; iy < 5; ++iy) + { + for (int ix = 0; ix < 5; ++ix) + { + MC c{}; + c.colorFactors = glm::vec4(0.82f, 0.82f, 0.82f, 1.0f); + c.metal_rough_factors = glm::vec4(mVals[ix], rVals[iy], 0.0f, 0.0f); + const std::string base = fmt::format("ibltest.m{}_r{}", ix, iy); + auto mat = eng->_assetManager->createMaterialFromConstants(base + ".mat", c, MaterialPass::MainColor); + + auto mesh = eng->_assetManager->createMesh(base + ".mesh", + std::span(verts.data(), verts.size()), + std::span(inds.data(), inds.size()), + mat); + + const glm::vec3 pos = origin + glm::vec3(ix * spacing, 0.5f, iy * spacing); + glm::mat4 M = glm::translate(glm::mat4(1.0f), pos); + eng->_sceneManager->addMeshInstance(base + ".inst", mesh, M, BoundsType::Sphere); + eng->_iblTestNames.push_back(base + ".inst"); + eng->_iblTestNames.push_back(base + ".mesh"); + eng->_iblTestNames.push_back(base + ".mat"); + } + } + + // Chrome and glass extras + { + MC chrome{}; + chrome.colorFactors = glm::vec4(0.9f, 0.9f, 0.9f, 1.0f); + chrome.metal_rough_factors = glm::vec4(1.0f, 0.06f, 0, 0); + auto mat = eng->_assetManager->createMaterialFromConstants("ibltest.chrome.mat", chrome, + MaterialPass::MainColor); + auto mesh = eng->_assetManager->createMesh("ibltest.chrome.mesh", std::span(verts.data(), verts.size()), std::span(inds.data(), inds.size()), mat); - - const glm::vec3 pos = origin + glm::vec3(ix * spacing, 0.5f, iy * spacing); - glm::mat4 M = glm::translate(glm::mat4(1.0f), pos); - eng->_sceneManager->addMeshInstance(base + ".inst", mesh, M, BoundsType::Sphere); - eng->_iblTestNames.push_back(base + ".inst"); - eng->_iblTestNames.push_back(base + ".mesh"); - eng->_iblTestNames.push_back(base + ".mat"); + glm::mat4 M = glm::translate(glm::mat4(1.0f), origin + glm::vec3(5.5f, 0.5f, 0.0f)); + eng->_sceneManager->addMeshInstance("ibltest.chrome.inst", mesh, M, BoundsType::Sphere); + eng->_iblTestNames.insert(eng->_iblTestNames.end(), + {"ibltest.chrome.inst", "ibltest.chrome.mesh", "ibltest.chrome.mat"}); + } { + MC glass{}; + glass.colorFactors = glm::vec4(0.9f, 0.95f, 1.0f, 0.25f); + glass.metal_rough_factors = glm::vec4(0.0f, 0.02f, 0, 0); + auto mat = eng->_assetManager->createMaterialFromConstants("ibltest.glass.mat", glass, + MaterialPass::Transparent); + auto mesh = eng->_assetManager->createMesh("ibltest.glass.mesh", + std::span(verts.data(), verts.size()), + std::span(inds.data(), inds.size()), + mat); + glm::mat4 M = glm::translate(glm::mat4(1.0f), origin + glm::vec3(5.5f, 0.5f, 2.0f)); + eng->_sceneManager->addMeshInstance("ibltest.glass.inst", mesh, M, BoundsType::Sphere); + eng->_iblTestNames.insert(eng->_iblTestNames.end(), + {"ibltest.glass.inst", "ibltest.glass.mesh", "ibltest.glass.mat"}); } } - // Chrome and glass extras + static void clear_ibl_test(VulkanEngine *eng) { - MC chrome{}; - chrome.colorFactors = glm::vec4(0.9f, 0.9f, 0.9f, 1.0f); - chrome.metal_rough_factors = glm::vec4(1.0f, 0.06f, 0, 0); - auto mat = eng->_assetManager->createMaterialFromConstants("ibltest.chrome.mat", chrome, MaterialPass::MainColor); - auto mesh = eng->_assetManager->createMesh("ibltest.chrome.mesh", - std::span(verts.data(), verts.size()), - std::span(inds.data(), inds.size()), - mat); - glm::mat4 M = glm::translate(glm::mat4(1.0f), origin + glm::vec3(5.5f, 0.5f, 0.0f)); - eng->_sceneManager->addMeshInstance("ibltest.chrome.inst", mesh, M, BoundsType::Sphere); - eng->_iblTestNames.insert(eng->_iblTestNames.end(), - {"ibltest.chrome.inst", "ibltest.chrome.mesh", "ibltest.chrome.mat"}); - } - { - MC glass{}; - glass.colorFactors = glm::vec4(0.9f, 0.95f, 1.0f, 0.25f); - glass.metal_rough_factors = glm::vec4(0.0f, 0.02f, 0, 0); - auto mat = eng->_assetManager->createMaterialFromConstants("ibltest.glass.mat", glass, MaterialPass::Transparent); - auto mesh = eng->_assetManager->createMesh("ibltest.glass.mesh", - std::span(verts.data(), verts.size()), - std::span(inds.data(), inds.size()), - mat); - glm::mat4 M = glm::translate(glm::mat4(1.0f), origin + glm::vec3(5.5f, 0.5f, 2.0f)); - eng->_sceneManager->addMeshInstance("ibltest.glass.inst", mesh, M, BoundsType::Sphere); - eng->_iblTestNames.insert(eng->_iblTestNames.end(), - {"ibltest.glass.inst", "ibltest.glass.mesh", "ibltest.glass.mat"}); - } -} - -static void clear_ibl_test(VulkanEngine *eng) -{ - if (!eng || !eng->_sceneManager || !eng->_assetManager) return; - for (size_t i = 0; i < eng->_iblTestNames.size(); ++i) - { - const std::string &n = eng->_iblTestNames[i]; - // Remove instances and meshes by prefix - if (n.ends_with(".inst")) eng->_sceneManager->removeMeshInstance(n); - else if (n.ends_with(".mesh")) eng->_assetManager->removeMesh(n); - } - eng->_iblTestNames.clear(); -} - -static void ui_ibl(VulkanEngine *eng) -{ - if (!eng) return; - if (ImGui::Button("Spawn IBL Test Grid")) { spawn_ibl_test(eng); } - ImGui::SameLine(); - if (ImGui::Button("Clear IBL Test")) { clear_ibl_test(eng); } - ImGui::TextUnformatted( - "5x5 spheres: metallic across columns, roughness across rows.\nExtra: chrome + glass."); -} - -// Quick stats & targets overview -static void ui_overview(VulkanEngine *eng) -{ - if (!eng) return; - ImGui::Text("frametime %.2f ms", eng->stats.frametime); - ImGui::Text("draw time %.2f ms", eng->stats.mesh_draw_time); - ImGui::Text("update time %.2f ms", eng->_sceneManager->stats.scene_update_time); - ImGui::Text("triangles %i", eng->stats.triangle_count); - ImGui::Text("draws %i", eng->stats.drawcall_count); - - ImGui::Separator(); - ImGui::Text("Draw extent: %ux%u", eng->_drawExtent.width, eng->_drawExtent.height); - auto scExt = eng->_swapchainManager->swapchainExtent(); - ImGui::Text("Swapchain: %ux%u", scExt.width, scExt.height); - ImGui::Text("Draw fmt: %s", string_VkFormat(eng->_swapchainManager->drawImage().imageFormat)); - ImGui::Text("Swap fmt: %s", string_VkFormat(eng->_swapchainManager->swapchainImageFormat())); -} - -// Texture streaming + budget UI -static const char *stateName(uint8_t s) -{ - switch (s) - { - case 0: return "Unloaded"; - case 1: return "Loading"; - case 2: return "Resident"; - case 3: return "Evicted"; - default: return "?"; - } -} - -static void ui_textures(VulkanEngine *eng) -{ - if (!eng || !eng->_textureCache) - { - ImGui::TextUnformatted("TextureCache not available"); - return; - } - DeviceManager *dev = eng->_deviceManager.get(); - VmaAllocator alloc = dev ? dev->allocator() : VK_NULL_HANDLE; - unsigned long long devLocalBudget = 0, devLocalUsage = 0; - if (alloc) - { - const VkPhysicalDeviceMemoryProperties *memProps = nullptr; - vmaGetMemoryProperties(alloc, &memProps); - VmaBudget budgets[VK_MAX_MEMORY_HEAPS] = {}; - vmaGetHeapBudgets(alloc, budgets); - if (memProps) + if (!eng || !eng->_sceneManager || !eng->_assetManager) return; + for (size_t i = 0; i < eng->_iblTestNames.size(); ++i) { - for (uint32_t i = 0; i < memProps->memoryHeapCount; ++i) + const std::string &n = eng->_iblTestNames[i]; + // Remove instances and meshes by prefix + if (n.ends_with(".inst")) eng->_sceneManager->removeMeshInstance(n); + else if (n.ends_with(".mesh")) eng->_assetManager->removeMesh(n); + } + eng->_iblTestNames.clear(); + } + + static void ui_ibl(VulkanEngine *eng) + { + if (!eng) return; + if (ImGui::Button("Spawn IBL Test Grid")) { spawn_ibl_test(eng); } + ImGui::SameLine(); + if (ImGui::Button("Clear IBL Test")) { clear_ibl_test(eng); } + ImGui::TextUnformatted( + "5x5 spheres: metallic across columns, roughness across rows.\nExtra: chrome + glass."); + } + + // Quick stats & targets overview + static void ui_overview(VulkanEngine *eng) + { + if (!eng) return; + ImGui::Text("frametime %.2f ms", eng->stats.frametime); + ImGui::Text("draw time %.2f ms", eng->stats.mesh_draw_time); + ImGui::Text("update time %.2f ms", eng->_sceneManager->stats.scene_update_time); + ImGui::Text("triangles %i", eng->stats.triangle_count); + ImGui::Text("draws %i", eng->stats.drawcall_count); + + ImGui::Separator(); + ImGui::Text("Draw extent: %ux%u", eng->_drawExtent.width, eng->_drawExtent.height); + auto scExt = eng->_swapchainManager->swapchainExtent(); + ImGui::Text("Swapchain: %ux%u", scExt.width, scExt.height); + ImGui::Text("Draw fmt: %s", string_VkFormat(eng->_swapchainManager->drawImage().imageFormat)); + ImGui::Text("Swap fmt: %s", string_VkFormat(eng->_swapchainManager->swapchainImageFormat())); + } + + // Texture streaming + budget UI + static const char *stateName(uint8_t s) + { + switch (s) + { + case 0: return "Unloaded"; + case 1: return "Loading"; + case 2: return "Resident"; + case 3: return "Evicted"; + default: return "?"; + } + } + + static void ui_textures(VulkanEngine *eng) + { + if (!eng || !eng->_textureCache) + { + ImGui::TextUnformatted("TextureCache not available"); + return; + } + DeviceManager *dev = eng->_deviceManager.get(); + VmaAllocator alloc = dev ? dev->allocator() : VK_NULL_HANDLE; + unsigned long long devLocalBudget = 0, devLocalUsage = 0; + if (alloc) + { + const VkPhysicalDeviceMemoryProperties *memProps = nullptr; + vmaGetMemoryProperties(alloc, &memProps); + VmaBudget budgets[VK_MAX_MEMORY_HEAPS] = {}; + vmaGetHeapBudgets(alloc, budgets); + if (memProps) { - if (memProps->memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) + for (uint32_t i = 0; i < memProps->memoryHeapCount; ++i) { - devLocalBudget += budgets[i].budget; - devLocalUsage += budgets[i].usage; + if (memProps->memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) + { + devLocalBudget += budgets[i].budget; + devLocalUsage += budgets[i].usage; + } } } } - } - const size_t texBudget = eng->query_texture_budget_bytes(); - eng->_textureCache->set_gpu_budget_bytes(texBudget); - const size_t resBytes = eng->_textureCache->resident_bytes(); - const size_t cpuSrcBytes = eng->_textureCache->cpu_source_bytes(); - ImGui::Text("Device local: %.1f / %.1f MiB", - (double) devLocalUsage / 1048576.0, - (double) devLocalBudget / 1048576.0); - ImGui::Text("Texture budget: %.1f MiB", (double) texBudget / 1048576.0); - ImGui::Text("Resident textures: %.1f MiB", (double) resBytes / 1048576.0); - ImGui::Text("CPU source bytes: %.1f MiB", (double) cpuSrcBytes / 1048576.0); - ImGui::SameLine(); - if (ImGui::Button("Trim To Budget Now")) - { - eng->_textureCache->evictToBudget(texBudget); - } - - // Controls - static int loadsPerPump = 4; - loadsPerPump = eng->_textureCache->max_loads_per_pump(); - if (ImGui::SliderInt("Loads/Frame", &loadsPerPump, 1, 16)) - { - eng->_textureCache->set_max_loads_per_pump(loadsPerPump); - } - static int uploadBudgetMiB = 128; - uploadBudgetMiB = (int) (eng->_textureCache->max_bytes_per_pump() / 1048576ull); - if (ImGui::SliderInt("Upload Budget (MiB)", &uploadBudgetMiB, 16, 2048)) - { - eng->_textureCache->set_max_bytes_per_pump((size_t) uploadBudgetMiB * 1048576ull); - } - static bool keepSources = false; - keepSources = eng->_textureCache->keep_source_bytes(); - if (ImGui::Checkbox("Keep Source Bytes", &keepSources)) - { - eng->_textureCache->set_keep_source_bytes(keepSources); - } - static int cpuBudgetMiB = 64; - cpuBudgetMiB = (int) (eng->_textureCache->cpu_source_budget() / 1048576ull); - if (ImGui::SliderInt("CPU Source Budget (MiB)", &cpuBudgetMiB, 0, 2048)) - { - eng->_textureCache->set_cpu_source_budget((size_t) cpuBudgetMiB * 1048576ull); - } - static int maxUploadDim = 4096; - maxUploadDim = (int) eng->_textureCache->max_upload_dimension(); - if (ImGui::SliderInt("Max Upload Dimension", &maxUploadDim, 0, 8192)) - { - eng->_textureCache->set_max_upload_dimension((uint32_t) std::max(0, maxUploadDim)); - } - - TextureCache::DebugStats stats{}; - std::vector rows; - eng->_textureCache->debug_snapshot(rows, stats); - ImGui::Text("Counts R:%zu U:%zu E:%zu", - stats.countResident, - stats.countUnloaded, - stats.countEvicted); - - const int topN = 12; - if (ImGui::BeginTable("texrows", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("MiB", ImGuiTableColumnFlags_WidthFixed, 80); - ImGui::TableSetupColumn("State", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("LastUsed", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("Name"); - ImGui::TableHeadersRow(); - int count = 0; - for (const auto &r : rows) + const size_t texBudget = eng->query_texture_budget_bytes(); + eng->_textureCache->set_gpu_budget_bytes(texBudget); + const size_t resBytes = eng->_textureCache->resident_bytes(); + const size_t cpuSrcBytes = eng->_textureCache->cpu_source_bytes(); + ImGui::Text("Device local: %.1f / %.1f MiB", + (double) devLocalUsage / 1048576.0, + (double) devLocalBudget / 1048576.0); + ImGui::Text("Texture budget: %.1f MiB", (double) texBudget / 1048576.0); + ImGui::Text("Resident textures: %.1f MiB", (double) resBytes / 1048576.0); + ImGui::Text("CPU source bytes: %.1f MiB", (double) cpuSrcBytes / 1048576.0); + ImGui::SameLine(); + if (ImGui::Button("Trim To Budget Now")) { - if (count++ >= topN) break; - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%.2f", (double) r.bytes / 1048576.0); - ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(stateName(r.state)); - ImGui::TableSetColumnIndex(2); - ImGui::Text("%u", r.lastUsed); - ImGui::TableSetColumnIndex(3); - ImGui::TextUnformatted(r.name.c_str()); + eng->_textureCache->evictToBudget(texBudget); } - ImGui::EndTable(); - } -} -// Shadows / Ray Query controls -static void ui_shadows(VulkanEngine *eng) -{ - if (!eng) return; - const bool rq = eng->_deviceManager->supportsRayQuery(); - const bool as = eng->_deviceManager->supportsAccelerationStructure(); - ImGui::Text("RayQuery: %s", rq ? "supported" : "not available"); - ImGui::Text("AccelStruct: %s", as ? "supported" : "not available"); - ImGui::Separator(); - - auto &ss = eng->_context->shadowSettings; - int mode = static_cast(ss.mode); - ImGui::TextUnformatted("Shadow Mode"); - ImGui::RadioButton("Clipmap only", &mode, 0); - ImGui::SameLine(); - ImGui::RadioButton("Clipmap + RT", &mode, 1); - ImGui::SameLine(); - ImGui::RadioButton("RT only", &mode, 2); - if (!(rq && as) && mode != 0) mode = 0; // guard for unsupported HW - ss.mode = static_cast(mode); - ss.hybridRayQueryEnabled = (ss.mode != 0); - - ImGui::BeginDisabled(ss.mode != 1u); - ImGui::TextUnformatted("Cascades using ray assist:"); - for (int i = 0; i < 4; ++i) - { - bool on = (ss.hybridRayCascadesMask >> i) & 1u; - std::string label = std::string("C") + std::to_string(i); - if (ImGui::Checkbox(label.c_str(), &on)) + // Controls + static int loadsPerPump = 4; + loadsPerPump = eng->_textureCache->max_loads_per_pump(); + if (ImGui::SliderInt("Loads/Frame", &loadsPerPump, 1, 16)) { - if (on) ss.hybridRayCascadesMask |= (1u << i); - else ss.hybridRayCascadesMask &= ~(1u << i); + eng->_textureCache->set_max_loads_per_pump(loadsPerPump); } - if (i != 3) ImGui::SameLine(); - } - ImGui::SliderFloat("N·L threshold", &ss.hybridRayNoLThreshold, 0.0f, 1.0f, "%.2f"); - ImGui::EndDisabled(); - - ImGui::Separator(); - ImGui::TextWrapped( - "Clipmap only: raster PCF+RPDB. Clipmap+RT: PCF assisted by ray query at low N·L. RT only: skip shadow maps and use ray tests only."); -} - -// Render Graph inspection (passes, images, buffers) -static void ui_render_graph(VulkanEngine *eng) -{ - if (!eng || !eng->_renderGraph) - { - ImGui::TextUnformatted("RenderGraph not available"); - return; - } - auto &graph = *eng->_renderGraph; - - std::vector passInfos; - graph.debug_get_passes(passInfos); - if (ImGui::Button("Reload Pipelines")) { eng->_pipelineManager->hotReloadChanged(); } - ImGui::SameLine(); - ImGui::Text("%zu passes", passInfos.size()); - - if (ImGui::BeginTable("passes", 8, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("Enable", ImGuiTableColumnFlags_WidthFixed, 70); - ImGui::TableSetupColumn("Name"); - ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80); - ImGui::TableSetupColumn("GPU ms", ImGuiTableColumnFlags_WidthFixed, 70); - ImGui::TableSetupColumn("CPU rec ms", ImGuiTableColumnFlags_WidthFixed, 90); - ImGui::TableSetupColumn("Imgs", ImGuiTableColumnFlags_WidthFixed, 55); - ImGui::TableSetupColumn("Bufs", ImGuiTableColumnFlags_WidthFixed, 55); - ImGui::TableSetupColumn("Attachments", ImGuiTableColumnFlags_WidthFixed, 100); - ImGui::TableHeadersRow(); - - auto typeName = [](RGPassType t) + static int uploadBudgetMiB = 128; + uploadBudgetMiB = (int) (eng->_textureCache->max_bytes_per_pump() / 1048576ull); + if (ImGui::SliderInt("Upload Budget (MiB)", &uploadBudgetMiB, 16, 2048)) { - switch (t) - { - case RGPassType::Graphics: return "Graphics"; - case RGPassType::Compute: return "Compute"; - case RGPassType::Transfer: return "Transfer"; - default: return "?"; - } - }; - - for (size_t i = 0; i < passInfos.size(); ++i) - { - auto &pi = passInfos[i]; - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - bool enabled = true; - if (auto it = eng->_rgPassToggles.find(pi.name); it != eng->_rgPassToggles.end()) enabled = it->second; - std::string chkId = std::string("##en") + std::to_string(i); - if (ImGui::Checkbox(chkId.c_str(), &enabled)) - { - eng->_rgPassToggles[pi.name] = enabled; - } - ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(pi.name.c_str()); - ImGui::TableSetColumnIndex(2); - ImGui::TextUnformatted(typeName(pi.type)); - ImGui::TableSetColumnIndex(3); - if (pi.gpuMillis >= 0.0f) ImGui::Text("%.2f", pi.gpuMillis); - else ImGui::TextUnformatted("-"); - ImGui::TableSetColumnIndex(4); - if (pi.cpuMillis >= 0.0f) ImGui::Text("%.2f", pi.cpuMillis); - else ImGui::TextUnformatted("-"); - ImGui::TableSetColumnIndex(5); - ImGui::Text("%u/%u", pi.imageReads, pi.imageWrites); - ImGui::TableSetColumnIndex(6); - ImGui::Text("%u/%u", pi.bufferReads, pi.bufferWrites); - ImGui::TableSetColumnIndex(7); - ImGui::Text("%u%s", pi.colorAttachmentCount, pi.hasDepth ? "+D" : ""); + eng->_textureCache->set_max_bytes_per_pump((size_t) uploadBudgetMiB * 1048576ull); } - ImGui::EndTable(); - } - - if (ImGui::CollapsingHeader("Images", ImGuiTreeNodeFlags_DefaultOpen)) - { - std::vector imgs; - graph.debug_get_images(imgs); - if (ImGui::BeginTable("images", 7, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + static bool keepSources = false; + keepSources = eng->_textureCache->keep_source_bytes(); + if (ImGui::Checkbox("Keep Source Bytes", &keepSources)) { - ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 40); + eng->_textureCache->set_keep_source_bytes(keepSources); + } + static int cpuBudgetMiB = 64; + cpuBudgetMiB = (int) (eng->_textureCache->cpu_source_budget() / 1048576ull); + if (ImGui::SliderInt("CPU Source Budget (MiB)", &cpuBudgetMiB, 0, 2048)) + { + eng->_textureCache->set_cpu_source_budget((size_t) cpuBudgetMiB * 1048576ull); + } + static int maxUploadDim = 4096; + maxUploadDim = (int) eng->_textureCache->max_upload_dimension(); + if (ImGui::SliderInt("Max Upload Dimension", &maxUploadDim, 0, 8192)) + { + eng->_textureCache->set_max_upload_dimension((uint32_t) std::max(0, maxUploadDim)); + } + + TextureCache::DebugStats stats{}; + std::vector rows; + eng->_textureCache->debug_snapshot(rows, stats); + ImGui::Text("Counts R:%zu U:%zu E:%zu", + stats.countResident, + stats.countUnloaded, + stats.countEvicted); + + const int topN = 12; + if (ImGui::BeginTable("texrows", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("MiB", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableSetupColumn("State", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("LastUsed", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("Name"); - ImGui::TableSetupColumn("Fmt", ImGuiTableColumnFlags_WidthFixed, 120); - ImGui::TableSetupColumn("Extent", ImGuiTableColumnFlags_WidthFixed, 120); - ImGui::TableSetupColumn("Imported", ImGuiTableColumnFlags_WidthFixed, 70); - ImGui::TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 80); - ImGui::TableSetupColumn("Life", ImGuiTableColumnFlags_WidthFixed, 80); ImGui::TableHeadersRow(); - for (const auto &im : imgs) + int count = 0; + for (const auto &r: rows) { + if (count++ >= topN) break; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - ImGui::Text("%u", im.id); + ImGui::Text("%.2f", (double) r.bytes / 1048576.0); ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(im.name.c_str()); + ImGui::TextUnformatted(stateName(r.state)); ImGui::TableSetColumnIndex(2); - ImGui::TextUnformatted(string_VkFormat(im.format)); + ImGui::Text("%u", r.lastUsed); ImGui::TableSetColumnIndex(3); - ImGui::Text("%ux%u", im.extent.width, im.extent.height); + ImGui::TextUnformatted(r.name.c_str()); + } + ImGui::EndTable(); + } + } + + // Shadows / Ray Query controls + static void ui_shadows(VulkanEngine *eng) + { + if (!eng) return; + const bool rq = eng->_deviceManager->supportsRayQuery(); + const bool as = eng->_deviceManager->supportsAccelerationStructure(); + ImGui::Text("RayQuery: %s", rq ? "supported" : "not available"); + ImGui::Text("AccelStruct: %s", as ? "supported" : "not available"); + ImGui::Separator(); + + auto &ss = eng->_context->shadowSettings; + int mode = static_cast(ss.mode); + ImGui::TextUnformatted("Shadow Mode"); + ImGui::RadioButton("Clipmap only", &mode, 0); + ImGui::SameLine(); + ImGui::RadioButton("Clipmap + RT", &mode, 1); + ImGui::SameLine(); + ImGui::RadioButton("RT only", &mode, 2); + if (!(rq && as) && mode != 0) mode = 0; // guard for unsupported HW + ss.mode = static_cast(mode); + ss.hybridRayQueryEnabled = (ss.mode != 0); + + ImGui::BeginDisabled(ss.mode != 1u); + ImGui::TextUnformatted("Cascades using ray assist:"); + for (int i = 0; i < 4; ++i) + { + bool on = (ss.hybridRayCascadesMask >> i) & 1u; + std::string label = std::string("C") + std::to_string(i); + if (ImGui::Checkbox(label.c_str(), &on)) + { + if (on) ss.hybridRayCascadesMask |= (1u << i); + else ss.hybridRayCascadesMask &= ~(1u << i); + } + if (i != 3) ImGui::SameLine(); + } + ImGui::SliderFloat("N·L threshold", &ss.hybridRayNoLThreshold, 0.0f, 1.0f, "%.2f"); + ImGui::EndDisabled(); + + ImGui::Separator(); + ImGui::TextWrapped( + "Clipmap only: raster PCF+RPDB. Clipmap+RT: PCF assisted by ray query at low N·L. RT only: skip shadow maps and use ray tests only."); + } + + // Render Graph inspection (passes, images, buffers) + static void ui_render_graph(VulkanEngine *eng) + { + if (!eng || !eng->_renderGraph) + { + ImGui::TextUnformatted("RenderGraph not available"); + return; + } + auto &graph = *eng->_renderGraph; + + std::vector passInfos; + graph.debug_get_passes(passInfos); + if (ImGui::Button("Reload Pipelines")) { eng->_pipelineManager->hotReloadChanged(); } + ImGui::SameLine(); + ImGui::Text("%zu passes", passInfos.size()); + + if (ImGui::BeginTable("passes", 8, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("Enable", ImGuiTableColumnFlags_WidthFixed, 70); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableSetupColumn("GPU ms", ImGuiTableColumnFlags_WidthFixed, 70); + ImGui::TableSetupColumn("CPU rec ms", ImGuiTableColumnFlags_WidthFixed, 90); + ImGui::TableSetupColumn("Imgs", ImGuiTableColumnFlags_WidthFixed, 55); + ImGui::TableSetupColumn("Bufs", ImGuiTableColumnFlags_WidthFixed, 55); + ImGui::TableSetupColumn("Attachments", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableHeadersRow(); + + auto typeName = [](RGPassType t) { + switch (t) + { + case RGPassType::Graphics: return "Graphics"; + case RGPassType::Compute: return "Compute"; + case RGPassType::Transfer: return "Transfer"; + default: return "?"; + } + }; + + for (size_t i = 0; i < passInfos.size(); ++i) + { + auto &pi = passInfos[i]; + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + bool enabled = true; + if (auto it = eng->_rgPassToggles.find(pi.name); it != eng->_rgPassToggles.end()) enabled = it->second; + std::string chkId = std::string("##en") + std::to_string(i); + if (ImGui::Checkbox(chkId.c_str(), &enabled)) + { + eng->_rgPassToggles[pi.name] = enabled; + } + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(pi.name.c_str()); + ImGui::TableSetColumnIndex(2); + ImGui::TextUnformatted(typeName(pi.type)); + ImGui::TableSetColumnIndex(3); + if (pi.gpuMillis >= 0.0f) ImGui::Text("%.2f", pi.gpuMillis); + else ImGui::TextUnformatted("-"); ImGui::TableSetColumnIndex(4); - ImGui::TextUnformatted(im.imported ? "yes" : "no"); + if (pi.cpuMillis >= 0.0f) ImGui::Text("%.2f", pi.cpuMillis); + else ImGui::TextUnformatted("-"); ImGui::TableSetColumnIndex(5); - ImGui::Text("0x%x", (unsigned) im.creationUsage); + ImGui::Text("%u/%u", pi.imageReads, pi.imageWrites); ImGui::TableSetColumnIndex(6); - ImGui::Text("%d..%d", im.firstUse, im.lastUse); + ImGui::Text("%u/%u", pi.bufferReads, pi.bufferWrites); + ImGui::TableSetColumnIndex(7); + ImGui::Text("%u%s", pi.colorAttachmentCount, pi.hasDepth ? "+D" : ""); } ImGui::EndTable(); } + + if (ImGui::CollapsingHeader("Images", ImGuiTreeNodeFlags_DefaultOpen)) + { + std::vector imgs; + graph.debug_get_images(imgs); + if (ImGui::BeginTable("images", 7, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 40); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Fmt", ImGuiTableColumnFlags_WidthFixed, 120); + ImGui::TableSetupColumn("Extent", ImGuiTableColumnFlags_WidthFixed, 120); + ImGui::TableSetupColumn("Imported", ImGuiTableColumnFlags_WidthFixed, 70); + ImGui::TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableSetupColumn("Life", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableHeadersRow(); + for (const auto &im: imgs) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%u", im.id); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(im.name.c_str()); + ImGui::TableSetColumnIndex(2); + ImGui::TextUnformatted(string_VkFormat(im.format)); + ImGui::TableSetColumnIndex(3); + ImGui::Text("%ux%u", im.extent.width, im.extent.height); + ImGui::TableSetColumnIndex(4); + ImGui::TextUnformatted(im.imported ? "yes" : "no"); + ImGui::TableSetColumnIndex(5); + ImGui::Text("0x%x", (unsigned) im.creationUsage); + ImGui::TableSetColumnIndex(6); + ImGui::Text("%d..%d", im.firstUse, im.lastUse); + } + ImGui::EndTable(); + } + } + + if (ImGui::CollapsingHeader("Buffers")) + { + std::vector bufs; + graph.debug_get_buffers(bufs); + if (ImGui::BeginTable("buffers", 6, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 40); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Imported", ImGuiTableColumnFlags_WidthFixed, 70); + ImGui::TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Life", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableHeadersRow(); + for (const auto &bf: bufs) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%u", bf.id); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(bf.name.c_str()); + ImGui::TableSetColumnIndex(2); + ImGui::Text("%zu", (size_t) bf.size); + ImGui::TableSetColumnIndex(3); + ImGui::TextUnformatted(bf.imported ? "yes" : "no"); + ImGui::TableSetColumnIndex(4); + ImGui::Text("0x%x", (unsigned) bf.usage); + ImGui::TableSetColumnIndex(5); + ImGui::Text("%d..%d", bf.firstUse, bf.lastUse); + } + ImGui::EndTable(); + } + } } - if (ImGui::CollapsingHeader("Buffers")) + // Pipeline manager (graphics) + static void ui_pipelines(VulkanEngine *eng) { - std::vector bufs; - graph.debug_get_buffers(bufs); - if (ImGui::BeginTable("buffers", 6, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + if (!eng || !eng->_pipelineManager) + { + ImGui::TextUnformatted("PipelineManager not available"); + return; + } + std::vector pipes; + eng->_pipelineManager->debug_get_graphics(pipes); + if (ImGui::Button("Reload Changed")) { eng->_pipelineManager->hotReloadChanged(); } + ImGui::SameLine(); + ImGui::Text("%zu graphics pipelines", pipes.size()); + if (ImGui::BeginTable("gfxpipes", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 40); ImGui::TableSetupColumn("Name"); - ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 100); - ImGui::TableSetupColumn("Imported", ImGuiTableColumnFlags_WidthFixed, 70); - ImGui::TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 100); - ImGui::TableSetupColumn("Life", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableSetupColumn("VS"); + ImGui::TableSetupColumn("FS"); + ImGui::TableSetupColumn("Valid", ImGuiTableColumnFlags_WidthFixed, 60); ImGui::TableHeadersRow(); - for (const auto &bf : bufs) + for (const auto &p: pipes) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - ImGui::Text("%u", bf.id); + ImGui::TextUnformatted(p.name.c_str()); ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(bf.name.c_str()); + ImGui::TextUnformatted(p.vertexShaderPath.c_str()); ImGui::TableSetColumnIndex(2); - ImGui::Text("%zu", (size_t) bf.size); + ImGui::TextUnformatted(p.fragmentShaderPath.c_str()); ImGui::TableSetColumnIndex(3); - ImGui::TextUnformatted(bf.imported ? "yes" : "no"); - ImGui::TableSetColumnIndex(4); - ImGui::Text("0x%x", (unsigned) bf.usage); - ImGui::TableSetColumnIndex(5); - ImGui::Text("%d..%d", bf.firstUse, bf.lastUse); + ImGui::TextUnformatted(p.valid ? "yes" : "no"); } ImGui::EndTable(); } } -} -// Pipeline manager (graphics) -static void ui_pipelines(VulkanEngine *eng) -{ - if (!eng || !eng->_pipelineManager) + // Post-processing + static void ui_postfx(VulkanEngine *eng) { - ImGui::TextUnformatted("PipelineManager not available"); - return; - } - std::vector pipes; - eng->_pipelineManager->debug_get_graphics(pipes); - if (ImGui::Button("Reload Changed")) { eng->_pipelineManager->hotReloadChanged(); } - ImGui::SameLine(); - ImGui::Text("%zu graphics pipelines", pipes.size()); - if (ImGui::BeginTable("gfxpipes", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) - { - ImGui::TableSetupColumn("Name"); - ImGui::TableSetupColumn("VS"); - ImGui::TableSetupColumn("FS"); - ImGui::TableSetupColumn("Valid", ImGuiTableColumnFlags_WidthFixed, 60); - ImGui::TableHeadersRow(); - for (const auto &p : pipes) + if (!eng) return; + if (auto *tm = eng->_renderPassManager ? eng->_renderPassManager->getPass() : nullptr) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(p.name.c_str()); - ImGui::TableSetColumnIndex(1); - ImGui::TextUnformatted(p.vertexShaderPath.c_str()); - ImGui::TableSetColumnIndex(2); - ImGui::TextUnformatted(p.fragmentShaderPath.c_str()); - ImGui::TableSetColumnIndex(3); - ImGui::TextUnformatted(p.valid ? "yes" : "no"); - } - ImGui::EndTable(); - } -} - -// Post-processing -static void ui_postfx(VulkanEngine *eng) -{ - if (!eng) return; - if (auto *tm = eng->_renderPassManager ? eng->_renderPassManager->getPass() : nullptr) - { - float exp = tm->exposure(); - int mode = tm->mode(); - if (ImGui::SliderFloat("Exposure", &exp, 0.05f, 8.0f)) { tm->setExposure(exp); } - ImGui::TextUnformatted("Operator"); - ImGui::SameLine(); - if (ImGui::RadioButton("Reinhard", mode == 0)) - { - mode = 0; - tm->setMode(mode); - } - ImGui::SameLine(); - if (ImGui::RadioButton("ACES", mode == 1)) - { - mode = 1; - tm->setMode(mode); - } - } - else - { - ImGui::TextUnformatted("Tonemap pass not available"); - } -} - -// Scene debug bits -static void ui_scene(VulkanEngine *eng) -{ - if (!eng) return; - const DrawContext &dc = eng->_context->getMainDrawContext(); - ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size()); - ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size()); - ImGui::Checkbox("Use ID-buffer picking", &eng->_useIdBufferPicking); - ImGui::Text("Picking mode: %s", - eng->_useIdBufferPicking ? "ID buffer (async, 1-frame latency)" : "CPU raycast"); - ImGui::Checkbox("Debug draw mesh BVH (last pick)", &eng->_debugDrawBVH); - ImGui::Separator(); - - if (eng->_lastPick.valid) - { - const char *meshName = eng->_lastPick.mesh ? eng->_lastPick.mesh->name.c_str() : ""; - const char *sceneName = ""; - if (eng->_lastPick.scene && !eng->_lastPick.scene->debugName.empty()) - { - sceneName = eng->_lastPick.scene->debugName.c_str(); - } - ImGui::Text("Last pick scene: %s", sceneName); - ImGui::Text("Last pick source: %s", - eng->_useIdBufferPicking ? "ID buffer" : "CPU raycast"); - ImGui::Text("Last pick object ID: %u", eng->_lastPickObjectID); - ImGui::Text("Last pick mesh: %s (surface %u)", meshName, eng->_lastPick.surfaceIndex); - ImGui::Text("World pos: (%.3f, %.3f, %.3f)", - eng->_lastPick.worldPos.x, - eng->_lastPick.worldPos.y, - eng->_lastPick.worldPos.z); - ImGui::Text("Indices: first=%u count=%u", - eng->_lastPick.firstIndex, - eng->_lastPick.indexCount); - - if (eng->_sceneManager) - { - const SceneManager::PickingDebug &dbg = eng->_sceneManager->getPickingDebug(); - ImGui::Text("Mesh BVH used: %s, hit: %s, fallback box: %s", - dbg.usedMeshBVH ? "yes" : "no", - dbg.meshBVHHit ? "yes" : "no", - dbg.meshBVHFallbackBox ? "yes" : "no"); - if (dbg.meshBVHPrimCount > 0) + float exp = tm->exposure(); + int mode = tm->mode(); + if (ImGui::SliderFloat("Exposure", &exp, 0.05f, 8.0f)) { tm->setExposure(exp); } + ImGui::TextUnformatted("Operator"); + ImGui::SameLine(); + if (ImGui::RadioButton("Reinhard", mode == 0)) { - ImGui::Text("Mesh BVH stats: prims=%u, nodes=%u", - dbg.meshBVHPrimCount, - dbg.meshBVHNodeCount); + mode = 0; + tm->setMode(mode); + } + ImGui::SameLine(); + if (ImGui::RadioButton("ACES", mode == 1)) + { + mode = 1; + tm->setMode(mode); } } + else + { + ImGui::TextUnformatted("Tonemap pass not available"); + } } - else - { - ImGui::TextUnformatted("Last pick: "); - } - ImGui::Separator(); - if (eng->_hoverPick.valid) - { - const char *meshName = eng->_hoverPick.mesh ? eng->_hoverPick.mesh->name.c_str() : ""; - ImGui::Text("Hover mesh: %s (surface %u)", meshName, eng->_hoverPick.surfaceIndex); - } - else - { - ImGui::TextUnformatted("Hover: "); - } - if (!eng->_dragSelection.empty()) - { - ImGui::Text("Drag selection: %zu objects", eng->_dragSelection.size()); - } -} + // Scene debug bits + static void ui_scene(VulkanEngine *eng) + { + if (!eng) return; + const DrawContext &dc = eng->_context->getMainDrawContext(); + ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size()); + ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size()); + ImGui::Checkbox("Use ID-buffer picking", &eng->_useIdBufferPicking); + ImGui::Text("Picking mode: %s", + eng->_useIdBufferPicking ? "ID buffer (async, 1-frame latency)" : "CPU raycast"); + ImGui::Checkbox("Debug draw mesh BVH (last pick)", &eng->_debugDrawBVH); + ImGui::Separator(); + + // Spawn glTF instances (runtime) + ImGui::TextUnformatted("Spawn glTF instance"); + static char gltfPath[256] = "mirage2000/scene.gltf"; + static char gltfName[128] = "gltf_01"; + static float gltfPos[3] = {0.0f, 0.0f, 0.0f}; + static float gltfRot[3] = {0.0f, 0.0f, 0.0f}; // pitch, yaw, roll (deg) + static float gltfScale[3] = {1.0f, 1.0f, 1.0f}; + ImGui::InputText("Model path (assets/models/...)", gltfPath, IM_ARRAYSIZE(gltfPath)); + ImGui::InputText("Instance name", gltfName, IM_ARRAYSIZE(gltfName)); + ImGui::InputFloat3("Position", gltfPos); + ImGui::InputFloat3("Rotation (deg XYZ)", gltfRot); + ImGui::InputFloat3("Scale", gltfScale); + if (ImGui::Button("Add glTF instance")) + { + glm::mat4 T = glm::translate(glm::mat4(1.0f), glm::vec3(gltfPos[0], gltfPos[1], gltfPos[2])); + glm::mat4 R = glm::eulerAngleXYZ(glm::radians(gltfRot[0]), + glm::radians(gltfRot[1]), + glm::radians(gltfRot[2])); + glm::mat4 S = glm::scale(glm::mat4(1.0f), glm::vec3(gltfScale[0], gltfScale[1], gltfScale[2])); + glm::mat4 M = T * R * S; + eng->addGLTFInstance(gltfName, gltfPath, M); + } + + ImGui::Separator(); + // Spawn primitive mesh instances (cube/sphere) + ImGui::TextUnformatted("Spawn primitive"); + static int primType = 0; // 0 = cube, 1 = sphere + static char primName[128] = "prim_01"; + static float primPos[3] = {0.0f, 0.0f, 0.0f}; + static float primRot[3] = {0.0f, 0.0f, 0.0f}; + static float primScale[3] = {1.0f, 1.0f, 1.0f}; + ImGui::RadioButton("Cube", &primType, 0); ImGui::SameLine(); + ImGui::RadioButton("Sphere", &primType, 1); + ImGui::InputText("Primitive name", primName, IM_ARRAYSIZE(primName)); + ImGui::InputFloat3("Prim Position", primPos); + ImGui::InputFloat3("Prim Rotation (deg XYZ)", primRot); + ImGui::InputFloat3("Prim Scale", primScale); + if (ImGui::Button("Add primitive instance")) + { + std::shared_ptr mesh = (primType == 0) ? eng->cubeMesh : eng->sphereMesh; + if (mesh) + { + glm::mat4 T = glm::translate(glm::mat4(1.0f), glm::vec3(primPos[0], primPos[1], primPos[2])); + glm::mat4 R = glm::eulerAngleXYZ(glm::radians(primRot[0]), + glm::radians(primRot[1]), + glm::radians(primRot[2])); + glm::mat4 S = glm::scale(glm::mat4(1.0f), glm::vec3(primScale[0], primScale[1], primScale[2])); + glm::mat4 M = T * R * S; + eng->_sceneManager->addMeshInstance(primName, mesh, M); + } + } + + ImGui::Separator(); + // Delete selected model/primitive (uses last pick if valid, otherwise hover) + static std::string deleteStatus; + if (ImGui::Button("Delete selected")) + { + deleteStatus.clear(); + const auto *pick = eng->_lastPick.valid ? &eng->_lastPick + : (eng->_hoverPick.valid ? &eng->_hoverPick : nullptr); + if (!pick || pick->ownerName.empty()) + { + deleteStatus = "No selection to delete."; + } + else if (pick->ownerType == RenderObject::OwnerType::MeshInstance) + { + bool ok = eng->_sceneManager->removeMeshInstance(pick->ownerName); + deleteStatus = ok ? "Removed mesh instance: " + pick->ownerName + : "Mesh instance not found: " + pick->ownerName; + } + else if (pick->ownerType == RenderObject::OwnerType::GLTFInstance) + { + bool ok = eng->_sceneManager->removeGLTFInstance(pick->ownerName); + deleteStatus = ok ? "Removed glTF instance: " + pick->ownerName + : "glTF instance not found: " + pick->ownerName; + } + else + { + deleteStatus = "Cannot delete this object type (static scene)."; + } + } + if (!deleteStatus.empty()) + { + ImGui::TextUnformatted(deleteStatus.c_str()); + } + ImGui::Separator(); + + if (eng->_lastPick.valid) + { + const char *meshName = eng->_lastPick.mesh ? eng->_lastPick.mesh->name.c_str() : ""; + const char *sceneName = ""; + if (eng->_lastPick.scene && !eng->_lastPick.scene->debugName.empty()) + { + sceneName = eng->_lastPick.scene->debugName.c_str(); + } + ImGui::Text("Last pick scene: %s", sceneName); + ImGui::Text("Last pick source: %s", + eng->_useIdBufferPicking ? "ID buffer" : "CPU raycast"); + ImGui::Text("Last pick object ID: %u", eng->_lastPickObjectID); + ImGui::Text("Last pick mesh: %s (surface %u)", meshName, eng->_lastPick.surfaceIndex); + ImGui::Text("World pos: (%.3f, %.3f, %.3f)", + eng->_lastPick.worldPos.x, + eng->_lastPick.worldPos.y, + eng->_lastPick.worldPos.z); + const char *ownerTypeStr = "none"; + switch (eng->_lastPick.ownerType) + { + case RenderObject::OwnerType::MeshInstance: ownerTypeStr = "mesh instance"; break; + case RenderObject::OwnerType::GLTFInstance: ownerTypeStr = "glTF instance"; break; + case RenderObject::OwnerType::StaticGLTF: ownerTypeStr = "glTF scene"; break; + default: break; + } + const char *ownerName = eng->_lastPick.ownerName.empty() ? "" : eng->_lastPick.ownerName.c_str(); + ImGui::Text("Owner: %s (%s)", ownerName, ownerTypeStr); + ImGui::Text("Indices: first=%u count=%u", + eng->_lastPick.firstIndex, + eng->_lastPick.indexCount); + + if (eng->_sceneManager) + { + const SceneManager::PickingDebug &dbg = eng->_sceneManager->getPickingDebug(); + ImGui::Text("Mesh BVH used: %s, hit: %s, fallback box: %s", + dbg.usedMeshBVH ? "yes" : "no", + dbg.meshBVHHit ? "yes" : "no", + dbg.meshBVHFallbackBox ? "yes" : "no"); + if (dbg.meshBVHPrimCount > 0) + { + ImGui::Text("Mesh BVH stats: prims=%u, nodes=%u", + dbg.meshBVHPrimCount, + dbg.meshBVHNodeCount); + } + } + } + else + { + ImGui::TextUnformatted("Last pick: "); + } + ImGui::Separator(); + if (eng->_hoverPick.valid) + { + const char *meshName = eng->_hoverPick.mesh ? eng->_hoverPick.mesh->name.c_str() : ""; + ImGui::Text("Hover mesh: %s (surface %u)", meshName, eng->_hoverPick.surfaceIndex); + const char *ownerTypeStr = "none"; + switch (eng->_hoverPick.ownerType) + { + case RenderObject::OwnerType::MeshInstance: ownerTypeStr = "mesh instance"; break; + case RenderObject::OwnerType::GLTFInstance: ownerTypeStr = "glTF instance"; break; + case RenderObject::OwnerType::StaticGLTF: ownerTypeStr = "glTF scene"; break; + default: break; + } + const char *ownerName = eng->_hoverPick.ownerName.empty() ? "" : eng->_hoverPick.ownerName.c_str(); + ImGui::Text("Hover owner: %s (%s)", ownerName, ownerTypeStr); + } + else + { + ImGui::TextUnformatted("Hover: "); + } + if (!eng->_dragSelection.empty()) + { + ImGui::Text("Drag selection: %zu objects", eng->_dragSelection.size()); + } + + ImGui::Separator(); + ImGui::TextUnformatted("Object Gizmo (ImGuizmo)"); + + if (!eng->_sceneManager) + { + ImGui::TextUnformatted("SceneManager not available"); + return; + } + + SceneManager *sceneMgr = eng->_sceneManager.get(); + const GPUSceneData &sceneData = sceneMgr->getSceneData(); + + static ImGuizmo::OPERATION op = ImGuizmo::TRANSLATE; + static ImGuizmo::MODE mode = ImGuizmo::LOCAL; + + ImGui::TextUnformatted("Operation"); + if (ImGui::RadioButton("Translate", op == ImGuizmo::TRANSLATE)) op = ImGuizmo::TRANSLATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Rotate", op == ImGuizmo::ROTATE)) op = ImGuizmo::ROTATE; + ImGui::SameLine(); + if (ImGui::RadioButton("Scale", op == ImGuizmo::SCALE)) op = ImGuizmo::SCALE; + + ImGui::TextUnformatted("Mode"); + if (ImGui::RadioButton("Local", mode == ImGuizmo::LOCAL)) mode = ImGuizmo::LOCAL; + ImGui::SameLine(); + if (ImGui::RadioButton("World", mode == ImGuizmo::WORLD)) mode = ImGuizmo::WORLD; + + // Resolve a dynamic instance transform for the current pick. + glm::mat4 targetTransform(1.0f); + enum class GizmoTarget + { + None, + MeshInstance, + GLTFInstance, + Node + }; + GizmoTarget target = GizmoTarget::None; + + ImGuiIO &io = ImGui::GetIO(); + ImGuizmo::SetOrthographic(false); + ImGuizmo::SetDrawlist(); + ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); + + glm::mat4 view = sceneData.view; + glm::mat4 proj = sceneData.proj; + + proj[1][1] *= -1.0f; + + ImGuizmo::Manipulate(&view[0][0], &proj[0][0], + op, mode, + &targetTransform[0][0]); + } } // namespace void vk_engine_draw_debug_ui(VulkanEngine *eng) { if (!eng) return; + ImGuizmo::BeginFrame(); + // Consolidated debug window with tabs if (ImGui::Begin("Debug")) { const ImGuiTabBarFlags tf = - ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs; + ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs; if (ImGui::BeginTabBar("DebugTabs", tf)) { if (ImGui::BeginTabItem("Overview")) diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index cf984b6..903af65 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -34,6 +34,9 @@ void SceneManager::update_scene() { auto start = std::chrono::system_clock::now(); + // Release any GLTF assets that were scheduled for safe destruction after GPU idle. + pendingGLTFRelease.clear(); + mainDrawContext.OpaqueSurfaces.clear(); mainDrawContext.TransparentSurfaces.clear(); mainDrawContext.nextID = 1; @@ -78,12 +81,30 @@ void SceneManager::update_scene() } } + auto tagOwner = [&](RenderObject::OwnerType type, const std::string &name, + size_t opaqueBegin, size_t transpBegin) + { + for (size_t i = opaqueBegin; i < mainDrawContext.OpaqueSurfaces.size(); ++i) + { + mainDrawContext.OpaqueSurfaces[i].ownerType = type; + mainDrawContext.OpaqueSurfaces[i].ownerName = name; + } + for (size_t i = transpBegin; i < mainDrawContext.TransparentSurfaces.size(); ++i) + { + mainDrawContext.TransparentSurfaces[i].ownerType = type; + mainDrawContext.TransparentSurfaces[i].ownerName = name; + } + }; + // Draw all loaded GLTF scenes (static world) for (auto &[name, scene] : loadedScenes) { if (scene) { + const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size(); + const size_t transpStart = mainDrawContext.TransparentSurfaces.size(); scene->Draw(glm::mat4{1.f}, mainDrawContext); + tagOwner(RenderObject::OwnerType::StaticGLTF, name, opaqueStart, transpStart); } } @@ -93,7 +114,10 @@ void SceneManager::update_scene() const GLTFInstance &inst = kv.second; if (inst.scene) { + const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size(); + const size_t transpStart = mainDrawContext.TransparentSurfaces.size(); inst.scene->Draw(inst.transform, mainDrawContext); + tagOwner(RenderObject::OwnerType::GLTFInstance, kv.first, opaqueStart, transpStart); } } @@ -123,6 +147,8 @@ void SceneManager::update_scene() obj.sourceMesh = inst.mesh.get(); obj.surfaceIndex = surfaceIndex++; obj.objectID = mainDrawContext.nextID++; + obj.ownerType = RenderObject::OwnerType::MeshInstance; + obj.ownerName = kv.first; if (obj.material->passType == MaterialPass::Transparent) { mainDrawContext.TransparentSurfaces.push_back(obj); @@ -299,7 +325,17 @@ void SceneManager::addGLTFInstance(const std::string &name, std::shared_ptr 0; + auto it = dynamicGLTFInstances.find(name); + if (it == dynamicGLTFInstances.end()) return false; + + // Defer destruction until after the next frame fence (update_scene). + if (it->second.scene) + { + pendingGLTFRelease.push_back(it->second.scene); + } + + dynamicGLTFInstances.erase(it); + return true; } bool SceneManager::setGLTFInstanceTransform(const std::string &name, const glm::mat4 &transform) @@ -312,6 +348,13 @@ bool SceneManager::setGLTFInstanceTransform(const std::string &name, const glm:: void SceneManager::clearGLTFInstances() { + for (auto &kv : dynamicGLTFInstances) + { + if (kv.second.scene) + { + pendingGLTFRelease.push_back(kv.second.scene); + } + } dynamicGLTFInstances.clear(); } diff --git a/src/scene/vk_scene.h b/src/scene/vk_scene.h index 2b99e8a..9d12912 100644 --- a/src/scene/vk_scene.h +++ b/src/scene/vk_scene.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "scene/vk_loader.h" class EngineContext; @@ -28,8 +29,19 @@ struct RenderObject uint32_t surfaceIndex = 0; // Unique per-draw identifier for ID-buffer picking (0 = none). uint32_t objectID = 0; - // Optional owning glTF scene for this draw (null for procedural/dynamic meshes). + // Optional logical owner for editor/picking (instance name etc.). + enum class OwnerType : uint8_t + { + None = 0, + StaticGLTF, // loaded scene + GLTFInstance, // runtime glTF instance with transform + MeshInstance // dynamic primitive/mesh instance + }; + OwnerType ownerType = OwnerType::None; + std::string ownerName; + // Optional owning glTF scene and node for this draw (null for procedural/dynamic meshes). LoadedGLTF *sourceScene = nullptr; + Node *sourceNode = nullptr; }; struct DrawContext @@ -137,6 +149,9 @@ private: std::unordered_map > loadedNodes; std::unordered_map dynamicMeshInstances; std::unordered_map dynamicGLTFInstances; + // Keep GLTF assets alive until after the next frame fence to avoid destroying + // GPU resources that might still be in-flight. + std::vector> pendingGLTFRelease; PickingDebug pickingDebug{}; };