From bb848b05172dceb246948f6610ca3a3132eebf69 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Tue, 18 Nov 2025 14:28:57 +0900 Subject: [PATCH] ADD: Bounding --- src/core/vk_engine.cpp | 589 +----------------------------- src/core/vk_engine_ui.cpp | 642 +++++++++++++++++++++++++++++++++ src/scene/vk_scene_picking.cpp | 381 +++++++++++++++++++ 3 files changed, 1029 insertions(+), 583 deletions(-) create mode 100644 src/core/vk_engine_ui.cpp create mode 100644 src/scene/vk_scene_picking.cpp diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 2c41586..ed576f2 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -49,532 +49,8 @@ #include "core/texture_cache.h" #include "core/ibl_manager.h" -static size_t query_texture_budget_bytes(DeviceManager* dev) -{ - if (!dev) return 512ull * 1024ull * 1024ull; // fallback - VmaAllocator alloc = dev->allocator(); - if (!alloc) return 512ull * 1024ull * 1024ull; - - const VkPhysicalDeviceMemoryProperties* memProps = nullptr; - vmaGetMemoryProperties(alloc, &memProps); - if (!memProps) return 512ull * 1024ull * 1024ull; - - VmaBudget budgets[VK_MAX_MEMORY_HEAPS] = {}; - vmaGetHeapBudgets(alloc, budgets); - - unsigned long long totalBudget = 0; - unsigned long long totalUsage = 0; - for (uint32_t i = 0; i < memProps->memoryHeapCount; ++i) - { - if (memProps->memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) - { - totalBudget += budgets[i].budget; - totalUsage += budgets[i].usage; - } - } - if (totalBudget == 0) return 512ull * 1024ull * 1024ull; - - // Reserve ~65% of VRAM for attachments, swapchain, meshes, AS, etc. - unsigned long long cap = static_cast(double(totalBudget) * 0.35); - - // If usage is already near the cap, still allow current textures to live; eviction will trim. - // Clamp to at least 128 MB, at most totalBudget. - unsigned long long minCap = 128ull * 1024ull * 1024ull; - if (cap < minCap) cap = minCap; - if (cap > totalBudget) cap = totalBudget; - return static_cast(cap); -} - -// -// ImGui helpers: keep UI code tidy and grouped in small functions. -// These render inside a single consolidated Debug window using tab items. -// (Original definitions are now compiled out; see core/vk_engine_ui.cpp.) -// -namespace { - // Background / compute playground - static void ui_background(VulkanEngine *eng) - { - 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); - } - - // 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) - { - 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); - 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_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) - { - for (uint32_t i = 0; i < memProps->memoryHeapCount; ++i) - { - if (memProps->memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) - { - devLocalBudget += budgets[i].budget; - devLocalUsage += budgets[i].usage; - } - } - } - } - - const size_t texBudget = query_texture_budget_bytes(dev); - 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) - { - 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()); - } - 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); - 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" : ""); - } - 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(); - } - } - } - - // Pipeline manager (graphics) - static void ui_pipelines(VulkanEngine *eng) - { - 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("Name"); - ImGui::TableSetupColumn("VS"); - ImGui::TableSetupColumn("FS"); - ImGui::TableSetupColumn("Valid", ImGuiTableColumnFlags_WidthFixed, 60); - ImGui::TableHeadersRow(); - for (const auto &p : pipes) - { - 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::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 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); - } - 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()); - } - } -} // namespace +// ImGui debug UI (tabs, inspectors, etc.) is implemented in core/vk_engine_ui.cpp. +void vk_engine_draw_debug_ui(VulkanEngine *eng); VulkanEngine *loadedEngine = nullptr; @@ -759,8 +235,8 @@ void VulkanEngine::init() // Try to load default IBL assets if present { IBLPaths ibl{}; - // ibl.specularCube = _assetManager->assetPath("ibl/docklands.ktx2"); - // ibl.diffuseCube = _assetManager->assetPath("ibl/docklands.ktx2"); // temporary: reuse if separate diffuse not provided + ibl.specularCube = _assetManager->assetPath("ibl/docklands.ktx2"); + ibl.diffuseCube = _assetManager->assetPath("ibl/docklands.ktx2"); // temporary: reuse if separate diffuse not provided ibl.brdfLut2D = _assetManager->assetPath("ibl/brdf_lut.ktx2"); _iblManager->load(ibl); } @@ -1408,61 +884,8 @@ void VulkanEngine::run() ImGui::NewFrame(); - // Consolidated debug window with tabs - if (ImGui::Begin("Debug")) - { - const ImGuiTabBarFlags tf = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs; - if (ImGui::BeginTabBar("DebugTabs", tf)) - { - if (ImGui::BeginTabItem("Overview")) - { - ui_overview(this); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Background")) - { - ui_background(this); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Shadows")) - { - ui_shadows(this); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Render Graph")) - { - ui_render_graph(this); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Pipelines")) - { - ui_pipelines(this); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("IBL")) - { - ui_ibl(this); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("PostFX")) - { - ui_postfx(this); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Scene")) - { - ui_scene(this); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Textures")) - { - ui_textures(this); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - ImGui::End(); - } + // Build the engine debug UI (tabs, inspectors, etc.). + vk_engine_draw_debug_ui(this); ImGui::Render(); draw(); diff --git a/src/core/vk_engine_ui.cpp b/src/core/vk_engine_ui.cpp new file mode 100644 index 0000000..eceab94 --- /dev/null +++ b/src/core/vk_engine_ui.cpp @@ -0,0 +1,642 @@ +// ImGui debug UI helpers for VulkanEngine. +// +// This file contains the immediate-mode ImGui widgets that expose engine +// statistics, render-graph inspection, texture streaming controls, etc. +// The main frame loop in vk_engine.cpp simply calls vk_engine_draw_debug_ui(). + +#include "vk_engine.h" + +#include "imgui.h" + +#include "render/primitives.h" +#include "vk_mem_alloc.h" +#include "render/vk_renderpass_tonemap.h" +#include "render/vk_renderpass_background.h" +#include "render/rg_graph.h" +#include "core/vk_pipeline_manager.h" +#include "core/texture_cache.h" +#include "core/ibl_manager.h" +#include "engine_context.h" + +namespace { + +// Background / compute playground +static void ui_background(VulkanEngine *eng) +{ + 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); +} + +// 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) + { + 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); + 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) + { + for (uint32_t i = 0; i < memProps->memoryHeapCount; ++i) + { + 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) + { + 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()); + } + 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); + 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" : ""); + } + 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(); + } + } +} + +// Pipeline manager (graphics) +static void ui_pipelines(VulkanEngine *eng) +{ + 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("Name"); + ImGui::TableSetupColumn("VS"); + ImGui::TableSetupColumn("FS"); + ImGui::TableSetupColumn("Valid", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableHeadersRow(); + for (const auto &p : pipes) + { + 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::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 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); + } + 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()); + } +} + +} // namespace + +void vk_engine_draw_debug_ui(VulkanEngine *eng) +{ + if (!eng) return; + + // Consolidated debug window with tabs + if (ImGui::Begin("Debug")) + { + const ImGuiTabBarFlags tf = + ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs; + if (ImGui::BeginTabBar("DebugTabs", tf)) + { + if (ImGui::BeginTabItem("Overview")) + { + ui_overview(eng); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Background")) + { + ui_background(eng); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Shadows")) + { + ui_shadows(eng); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Render Graph")) + { + ui_render_graph(eng); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Pipelines")) + { + ui_pipelines(eng); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("IBL")) + { + ui_ibl(eng); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("PostFX")) + { + ui_postfx(eng); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Scene")) + { + ui_scene(eng); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Textures")) + { + ui_textures(eng); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + ImGui::End(); + } +} + diff --git a/src/scene/vk_scene_picking.cpp b/src/scene/vk_scene_picking.cpp new file mode 100644 index 0000000..33458ea --- /dev/null +++ b/src/scene/vk_scene_picking.cpp @@ -0,0 +1,381 @@ +#include "vk_scene.h" + +#include "vk_swapchain.h" +#include "core/engine_context.h" + +#include "glm/gtx/transform.hpp" +#include +#include "glm/gtx/norm.inl" + +#include +#include +#include +#include + +namespace +{ + // Ray / oriented-box intersection in world space using object-local AABB. + // Returns true when hit; outWorldHit is the closest hit point in world space. + bool intersect_ray_box(const glm::vec3 &rayOrigin, + const glm::vec3 &rayDir, + const Bounds &bounds, + const glm::mat4 &worldTransform, + glm::vec3 &outWorldHit) + { + if (glm::length2(rayDir) < 1e-8f) + { + return false; + } + + // Transform ray into local space of the bounds for precise box test. + glm::mat4 invM = glm::inverse(worldTransform); + glm::vec3 localOrigin = glm::vec3(invM * glm::vec4(rayOrigin, 1.0f)); + glm::vec3 localDir = glm::vec3(invM * glm::vec4(rayDir, 0.0f)); + + if (glm::length2(localDir) < 1e-8f) + { + return false; + } + localDir = glm::normalize(localDir); + + glm::vec3 minB = bounds.origin - bounds.extents; + glm::vec3 maxB = bounds.origin + bounds.extents; + + float tMin = 0.0f; + float tMax = std::numeric_limits::max(); + + for (int axis = 0; axis < 3; ++axis) + { + float o = localOrigin[axis]; + float d = localDir[axis]; + if (std::abs(d) < 1e-8f) + { + // Ray parallel to slab: must be inside to intersect. + if (o < minB[axis] || o > maxB[axis]) + { + return false; + } + } + else + { + float invD = 1.0f / d; + float t1 = (minB[axis] - o) * invD; + float t2 = (maxB[axis] - o) * invD; + if (t1 > t2) + { + std::swap(t1, t2); + } + + tMin = std::max(tMin, t1); + tMax = std::min(tMax, t2); + + if (tMax < tMin) + { + return false; + } + } + } + + if (tMax < 0.0f) + { + return false; + } + + float tHit = (tMin >= 0.0f) ? tMin : tMax; + glm::vec3 localHit = localOrigin + tHit * localDir; + glm::vec3 worldHit = glm::vec3(worldTransform * glm::vec4(localHit, 1.0f)); + + if (glm::dot(worldHit - rayOrigin, rayDir) <= 0.0f) + { + return false; + } + + outWorldHit = worldHit; + return true; + } + + // Quick conservative ray / bounding-sphere test in world space. + // Returns false when the ray misses the sphere; on hit, outT is the + // closest positive intersection distance along the ray direction. + bool intersect_ray_sphere(const glm::vec3 &rayOrigin, + const glm::vec3 &rayDir, + const Bounds &bounds, + const glm::mat4 &worldTransform, + float &outT) + { + // Sphere center is bounds.origin transformed to world. + glm::vec3 centerWorld = glm::vec3(worldTransform * glm::vec4(bounds.origin, 1.0f)); + + // Approximate world-space radius by scaling with the maximum axis scale. + glm::vec3 sx = glm::vec3(worldTransform[0]); + glm::vec3 sy = glm::vec3(worldTransform[1]); + glm::vec3 sz = glm::vec3(worldTransform[2]); + float maxScale = std::max({glm::length(sx), glm::length(sy), glm::length(sz)}); + float radiusWorld = bounds.sphereRadius * maxScale; + if (radiusWorld <= 0.0f) + { + return false; + } + + glm::vec3 oc = rayOrigin - centerWorld; + float b = glm::dot(oc, rayDir); + float c = glm::dot(oc, oc) - radiusWorld * radiusWorld; + float disc = b * b - c; + if (disc < 0.0f) + { + return false; + } + float s = std::sqrt(disc); + float t0 = -b - s; + float t1 = -b + s; + float t = t0 >= 0.0f ? t0 : t1; + if (t < 0.0f) + { + return false; + } + outT = t; + return true; + } + + // Ray / oriented-bounds intersection in world space using object-local shape. + // Uses a quick sphere test first; on success refines based on BoundsType. + // Returns true when hit; outWorldHit is the closest hit point in world space. + bool intersect_ray_bounds(const glm::vec3 &rayOrigin, + const glm::vec3 &rayDir, + const Bounds &bounds, + const glm::mat4 &worldTransform, + glm::vec3 &outWorldHit) + { + // Non-pickable object. + if (bounds.type == BoundsType::None) + { + return false; + } + + if (glm::length2(rayDir) < 1e-8f) + { + return false; + } + + // Early reject using bounding sphere in world space. + float sphereT = 0.0f; + if (!intersect_ray_sphere(rayOrigin, rayDir, bounds, worldTransform, sphereT)) + { + return false; + } + + // Shape-specific refinement after the conservative sphere test. + switch (bounds.type) + { + case BoundsType::Sphere: + { + // We already have the hit distance along the ray from the sphere test. + outWorldHit = rayOrigin + rayDir * sphereT; + return true; + } + case BoundsType::Box: + case BoundsType::Mesh: // TODO: replace with BVH/mesh query; box is a safe fallback. + case BoundsType::Capsule: + default: + { + // For Capsule and Mesh we currently fall back to the oriented box; + // this still benefits from tighter AABBs if you author them. + return intersect_ray_box(rayOrigin, rayDir, bounds, worldTransform, outWorldHit); + } + } + } + + // Test whether the clip-space box corners of an object intersect a 2D NDC rectangle. + // ndcMin/ndcMax are in [-1,1]x[-1,1]. Returns true if any visible corner projects inside. + bool box_overlaps_ndc_rect(const RenderObject &obj, + const glm::mat4 &viewproj, + const glm::vec2 &ndcMin, + const glm::vec2 &ndcMax) + { + const glm::vec3 o = obj.bounds.origin; + const glm::vec3 e = obj.bounds.extents; + const glm::mat4 m = viewproj * obj.transform; // world -> clip + + const std::array corners{ + glm::vec3{+1, +1, +1}, glm::vec3{+1, +1, -1}, glm::vec3{+1, -1, +1}, glm::vec3{+1, -1, -1}, + glm::vec3{-1, +1, +1}, glm::vec3{-1, +1, -1}, glm::vec3{-1, -1, +1}, glm::vec3{-1, -1, -1}, + }; + + for (const glm::vec3 &c : corners) + { + glm::vec3 pLocal = o + c * e; + glm::vec4 clip = m * glm::vec4(pLocal, 1.f); + if (clip.w <= 0.0f) + { + continue; + } + float x = clip.x / clip.w; + float y = clip.y / clip.w; + float z = clip.z / clip.w; // Vulkan Z0: 0..1 + if (z < 0.0f || z > 1.0f) + { + continue; + } + if (x >= ndcMin.x && x <= ndcMax.x && + y >= ndcMin.y && y <= ndcMax.y) + { + return true; + } + } + return false; + } +} // namespace + +bool SceneManager::pick(const glm::vec2 &mousePosPixels, RenderObject &outObject, glm::vec3 &outWorldPos) +{ + if (_context == nullptr) + { + return false; + } + + SwapchainManager *swapchain = _context->getSwapchain(); + if (swapchain == nullptr) + { + return false; + } + + VkExtent2D extent = swapchain->windowExtent(); + if (extent.width == 0 || extent.height == 0) + { + return false; + } + + float width = static_cast(extent.width); + float height = static_cast(extent.height); + + // Convert from window coordinates (top-left origin) to NDC in [-1, 1]. + float ndcX = (2.0f * mousePosPixels.x / width) - 1.0f; + float ndcY = 1.0f - (2.0f * mousePosPixels.y / height); + + float fovRad = glm::radians(mainCamera.fovDegrees); + float tanHalfFov = std::tan(fovRad * 0.5f); + float aspect = width / height; + + // Build ray in camera space using -Z forward convention. + glm::vec3 dirCamera(ndcX * aspect * tanHalfFov, + ndcY * tanHalfFov, + -1.0f); + dirCamera = glm::normalize(dirCamera); + + glm::vec3 rayOrigin = mainCamera.position; + glm::mat4 camRotation = mainCamera.getRotationMatrix(); + glm::vec3 rayDir = glm::normalize(glm::vec3(camRotation * glm::vec4(dirCamera, 0.0f))); + + bool anyHit = false; + float bestDist2 = std::numeric_limits::max(); + glm::vec3 bestHitPos{}; + + auto testList = [&](const std::vector &list) + { + for (const RenderObject &obj: list) + { + glm::vec3 hitPos{}; + if (!intersect_ray_bounds(rayOrigin, rayDir, obj.bounds, obj.transform, hitPos)) + { + continue; + } + + float d2 = glm::length2(hitPos - rayOrigin); + if (d2 < bestDist2) + { + bestDist2 = d2; + bestHitPos = hitPos; + outObject = obj; + anyHit = true; + } + } + }; + + testList(mainDrawContext.OpaqueSurfaces); + testList(mainDrawContext.TransparentSurfaces); + + if (anyHit) + { + outWorldPos = bestHitPos; + } + + return anyHit; +} + +bool SceneManager::resolveObjectID(uint32_t id, RenderObject &outObject) const +{ + if (id == 0) + { + return false; + } + + auto findIn = [&](const std::vector &list) -> bool + { + for (const RenderObject &obj : list) + { + if (obj.objectID == id) + { + outObject = obj; + return true; + } + } + return false; + }; + + if (findIn(mainDrawContext.OpaqueSurfaces)) + { + return true; + } + if (findIn(mainDrawContext.TransparentSurfaces)) + { + return true; + } + return false; +} + +void SceneManager::selectRect(const glm::vec2 &p0, const glm::vec2 &p1, std::vector &outObjects) const +{ + if (!_context || !_context->getSwapchain()) + { + return; + } + + VkExtent2D extent = _context->getSwapchain()->windowExtent(); + if (extent.width == 0 || extent.height == 0) + { + return; + } + + float width = static_cast(extent.width); + float height = static_cast(extent.height); + + // Convert from window coordinates (top-left origin) to NDC in [-1, 1]. + auto toNdc = [&](const glm::vec2 &p) -> glm::vec2 + { + float ndcX = (2.0f * p.x / width) - 1.0f; + float ndcY = 1.0f - (2.0f * p.y / height); + return glm::vec2{ndcX, ndcY}; + }; + + glm::vec2 ndc0 = toNdc(p0); + glm::vec2 ndc1 = toNdc(p1); + glm::vec2 ndcMin = glm::min(ndc0, ndc1); + glm::vec2 ndcMax = glm::max(ndc0, ndc1); + + const glm::mat4 vp = sceneData.viewproj; + + auto testList = [&](const std::vector &list) + { + for (const RenderObject &obj : list) + { + if (box_overlaps_ndc_rect(obj, vp, ndcMin, ndcMax)) + { + outObjects.push_back(obj); + } + } + }; + + testList(mainDrawContext.OpaqueSurfaces); + testList(mainDrawContext.TransparentSurfaces); +} +