// 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 "engine.h" #include "core/picking/picking_system.h" #include "core/debug_draw/debug_draw.h" #include "SDL2/SDL.h" #include "SDL2/SDL_vulkan.h" #include "imgui.h" #include "ImGuizmo.h" #include "render/primitives.h" #include "vk_mem_alloc.h" #include "render/passes/tonemap.h" #include "render/passes/fxaa.h" #include "render/passes/background.h" #include "render/passes/particles.h" #include #include #include #include "render/graph/graph.h" #include "core/pipeline/manager.h" #include "core/assets/texture_cache.h" #include "core/assets/ibl_manager.h" #include "device/images.h" #include "context.h" #include #include #include #include #include #include #include "mesh_bvh.h" #include "scene/planet/planet_system.h" namespace { static IBLPaths resolve_ibl_paths(VulkanEngine *eng, const IBLPaths &paths) { IBLPaths out = paths; if (!eng || !eng->_assetManager) return out; if (!out.specularCube.empty()) out.specularCube = eng->_assetManager->assetPath(out.specularCube); if (!out.diffuseCube.empty()) out.diffuseCube = eng->_assetManager->assetPath(out.diffuseCube); if (!out.brdfLut2D.empty()) out.brdfLut2D = eng->_assetManager->assetPath(out.brdfLut2D); if (!out.background2D.empty()) out.background2D = eng->_assetManager->assetPath(out.background2D); return out; } static void ui_window(VulkanEngine *eng) { if (!eng || !eng->_window) return; int num_displays = SDL_GetNumVideoDisplays(); if (num_displays <= 0) { ImGui::Text("No displays reported by SDL (%s)", SDL_GetError()); return; } int current_display = SDL_GetWindowDisplayIndex(eng->_window); if (current_display < 0) current_display = eng->_windowDisplayIndex; current_display = std::clamp(current_display, 0, num_displays - 1); const char *cur_display_name = SDL_GetDisplayName(current_display); if (!cur_display_name) cur_display_name = "Unknown"; ImGui::Text("Current: %s on display %d (%s)", (eng->_windowMode == VulkanEngine::WindowMode::Windowed) ? "Windowed" : (eng->_windowMode == VulkanEngine::WindowMode::FullscreenDesktop) ? "Borderless Fullscreen" : "Exclusive Fullscreen", current_display, cur_display_name); static int pending_display = -1; static int pending_mode = -1; // 0 windowed, 1 borderless, 2 exclusive if (pending_display < 0) pending_display = current_display; if (pending_mode < 0) pending_mode = static_cast(eng->_windowMode); ImGui::Separator(); if (ImGui::BeginCombo("Monitor", cur_display_name)) { for (int i = 0; i < num_displays; ++i) { const char *name = SDL_GetDisplayName(i); if (!name) name = "Unknown"; const bool selected = (pending_display == i); if (ImGui::Selectable(name, selected)) { pending_display = i; } if (selected) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } const char *mode_labels[] = { "Windowed", "Borderless (Fullscreen Desktop)", "Exclusive Fullscreen" }; ImGui::Combo("Mode", &pending_mode, mode_labels, 3); ImGui::TextUnformatted("Apply triggers immediate swapchain recreation."); if (ImGui::Button("Apply")) { auto mode = static_cast(std::clamp(pending_mode, 0, 2)); eng->set_window_mode(mode, pending_display); // Re-sync pending selections with what SDL actually applied. pending_display = SDL_GetWindowDisplayIndex(eng->_window); if (pending_display < 0) pending_display = eng->_windowDisplayIndex; pending_display = std::clamp(pending_display, 0, num_displays - 1); pending_mode = static_cast(eng->_windowMode); } ImGui::SameLine(); if (ImGui::Button("Use Current")) { pending_display = current_display; pending_mode = static_cast(eng->_windowMode); } ImGui::Separator(); ImGui::TextUnformatted("HiDPI / Sizes"); ImGui::Text("HiDPI enabled: %s", eng->_hiDpiEnabled ? "yes" : "no"); int winW = 0, winH = 0; SDL_GetWindowSize(eng->_window, &winW, &winH); int drawW = 0, drawH = 0; SDL_Vulkan_GetDrawableSize(eng->_window, &drawW, &drawH); ImGui::Text("Window size: %d x %d", winW, winH); ImGui::Text("Drawable size: %d x %d", drawW, drawH); if (winW > 0 && winH > 0 && drawW > 0 && drawH > 0) { ImGui::Text("Drawable scale: %.3f x %.3f", static_cast(drawW) / static_cast(winW), static_cast(drawH) / static_cast(winH)); } if (eng->_swapchainManager) { VkExtent2D sw = eng->_swapchainManager->swapchainExtent(); ImGui::Text("Swapchain extent: %u x %u", sw.width, sw.height); } ImGui::Separator(); ImGui::TextUnformatted("GPU"); if (!eng->_deviceManager || !eng->_deviceManager->physicalDevice()) { ImGui::TextUnformatted("No Vulkan device initialized."); return; } VkPhysicalDevice gpu = eng->_deviceManager->physicalDevice(); VkPhysicalDeviceProperties props{}; vkGetPhysicalDeviceProperties(gpu, &props); VkPhysicalDeviceMemoryProperties mem{}; vkGetPhysicalDeviceMemoryProperties(gpu, &mem); auto type_str = [](VkPhysicalDeviceType t) -> const char* { switch (t) { case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "Discrete"; case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "Integrated"; case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "Virtual"; case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU"; default: return "Other"; } }; uint64_t device_local_bytes = 0; for (uint32_t i = 0; i < mem.memoryHeapCount; ++i) { if (mem.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) { device_local_bytes += mem.memoryHeaps[i].size; } } const double vram_gib = static_cast(device_local_bytes) / (1024.0 * 1024.0 * 1024.0); const uint32_t api = props.apiVersion; ImGui::Text("Name: %s", props.deviceName); ImGui::Text("Type: %s", type_str(props.deviceType)); ImGui::Text("Vendor: 0x%04x Device: 0x%04x", props.vendorID, props.deviceID); ImGui::Text("Vulkan API: %u.%u.%u", VK_VERSION_MAJOR(api), VK_VERSION_MINOR(api), VK_VERSION_PATCH(api)); ImGui::Text("Driver: %u (0x%08x)", props.driverVersion, props.driverVersion); ImGui::Text("Device-local memory: %.2f GiB", vram_gib); ImGui::Text("RayQuery: %s AccelStruct: %s", eng->_deviceManager->supportsRayQuery() ? "yes" : "no", eng->_deviceManager->supportsAccelerationStructure() ? "yes" : "no"); } // 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::TextUnformatted("Render Resolution"); static int pendingLogicalW = 0; static int pendingLogicalH = 0; if (pendingLogicalW <= 0 || pendingLogicalH <= 0) { pendingLogicalW = static_cast(eng->_logicalRenderExtent.width); pendingLogicalH = static_cast(eng->_logicalRenderExtent.height); } ImGui::InputInt("Logical Width", &pendingLogicalW); ImGui::InputInt("Logical Height", &pendingLogicalH); if (ImGui::Button("Apply Logical Resolution")) { uint32_t w = static_cast(pendingLogicalW > 0 ? pendingLogicalW : 1); uint32_t h = static_cast(pendingLogicalH > 0 ? pendingLogicalH : 1); eng->set_logical_render_extent(VkExtent2D{w, h}); } ImGui::SameLine(); if (ImGui::Button("720p")) { pendingLogicalW = 1280; pendingLogicalH = 720; } ImGui::SameLine(); if (ImGui::Button("1080p")) { pendingLogicalW = 1920; pendingLogicalH = 1080; } ImGui::SameLine(); if (ImGui::Button("1440p")) { pendingLogicalW = 2560; pendingLogicalH = 1440; } static float pendingScale = 1.0f; if (!ImGui::IsAnyItemActive()) { pendingScale = eng->renderScale; } bool scaleChanged = ImGui::SliderFloat("Render Scale", &pendingScale, 0.25f, 2.0f); bool applyScale = scaleChanged && ImGui::IsItemDeactivatedAfterEdit(); ImGui::SameLine(); applyScale = ImGui::Button("Apply Scale") || applyScale; if (applyScale) { eng->set_render_scale(pendingScale); } } static void ui_particles(VulkanEngine *eng) { if (!eng || !eng->_renderPassManager) return; auto *pass = eng->_renderPassManager->getPass(); if (!pass) { ImGui::TextUnformatted("Particle pass not available"); return; } const uint32_t freeCount = pass->free_particles(); const uint32_t allocCount = pass->allocated_particles(); ImGui::Text("Pool: %u allocated / %u free (max %u)", allocCount, freeCount, ParticlePass::k_max_particles); ImGui::Separator(); static int newCount = 32768; newCount = std::max(newCount, 1); ImGui::InputInt("New System Particles", &newCount); ImGui::SameLine(); if (ImGui::Button("Create")) { const uint32_t want = static_cast(std::max(1, newCount)); pass->create_system(want); } ImGui::SameLine(); if (ImGui::Button("Create 32k")) { pass->create_system(32768); } ImGui::SameLine(); if (ImGui::Button("Create 128k")) { pass->create_system(ParticlePass::k_max_particles); } ImGui::Separator(); auto &systems = pass->systems(); if (systems.empty()) { ImGui::TextUnformatted("No particle systems. Create one above."); return; } static int selected = 0; selected = std::clamp(selected, 0, (int)systems.size() - 1); if (ImGui::BeginListBox("Systems")) { for (int i = 0; i < (int)systems.size(); ++i) { const auto &s = systems[i]; char label[128]; std::snprintf(label, sizeof(label), "#%u base=%u count=%u %s", s.id, s.base, s.count, s.enabled ? "on" : "off"); const bool isSelected = (selected == i); if (ImGui::Selectable(label, isSelected)) { selected = i; } if (isSelected) ImGui::SetItemDefaultFocus(); } ImGui::EndListBox(); } selected = std::clamp(selected, 0, (int)systems.size() - 1); auto &s = systems[(size_t)selected]; static std::vector vfxKtx2; auto refresh_vfx_list = [&]() { vfxKtx2.clear(); vfxKtx2.push_back(std::string{}); // None if (!eng || !eng->_assetManager) return; const auto &paths = eng->_assetManager->paths(); if (paths.assets.empty()) return; std::error_code ec; std::filesystem::path vfxDir = paths.assets / "vfx"; if (!std::filesystem::exists(vfxDir, ec) || ec) return; for (const auto &entry : std::filesystem::directory_iterator(vfxDir, ec)) { if (ec) break; if (!entry.is_regular_file(ec) || ec) continue; const auto p = entry.path(); if (p.extension() != ".ktx2" && p.extension() != ".KTX2") continue; vfxKtx2.push_back(std::string("vfx/") + p.filename().string()); } std::sort(vfxKtx2.begin() + 1, vfxKtx2.end()); }; if (vfxKtx2.empty()) { refresh_vfx_list(); } ImGui::Separator(); ImGui::Text("Selected: id=%u base=%u count=%u", s.id, s.base, s.count); ImGui::Checkbox("Enabled", &s.enabled); ImGui::SameLine(); if (ImGui::Button("Reset (Respawn)")) { s.reset = true; } ImGui::SameLine(); if (ImGui::Button("Destroy")) { const uint32_t id = s.id; pass->destroy_system(id); selected = 0; return; } const char *blendItems[] = {"Additive", "Alpha (block-sorted)"}; int blend = (s.blend == ParticlePass::BlendMode::Alpha) ? 1 : 0; if (ImGui::Combo("Blend", &blend, blendItems, 2)) { s.blend = (blend == 1) ? ParticlePass::BlendMode::Alpha : ParticlePass::BlendMode::Additive; } ImGui::Separator(); static int pendingResizeCount = 0; if (!ImGui::IsAnyItemActive()) { pendingResizeCount = (int)s.count; } ImGui::InputInt("Resize Count", &pendingResizeCount); ImGui::SameLine(); if (ImGui::Button("Apply Resize")) { const uint32_t want = static_cast(std::max(0, pendingResizeCount)); pass->resize_system(s.id, want); } ImGui::Separator(); ImGui::TextUnformatted("Emitter"); ImGui::InputFloat3("Position (local)", reinterpret_cast(&s.params.emitter_pos_local)); ImGui::SliderFloat("Spawn Radius", &s.params.spawn_radius, 0.0f, 10.0f, "%.3f"); ImGui::InputFloat3("Direction (local)", reinterpret_cast(&s.params.emitter_dir_local)); ImGui::SliderFloat("Cone Angle (deg)", &s.params.cone_angle_degrees, 0.0f, 89.0f, "%.1f"); ImGui::Separator(); ImGui::TextUnformatted("Motion"); ImGui::InputFloat("Min Speed", &s.params.min_speed); ImGui::InputFloat("Max Speed", &s.params.max_speed); ImGui::InputFloat("Min Life (s)", &s.params.min_life); ImGui::InputFloat("Max Life (s)", &s.params.max_life); ImGui::InputFloat("Min Size", &s.params.min_size); ImGui::InputFloat("Max Size", &s.params.max_size); ImGui::SliderFloat("Drag", &s.params.drag, 0.0f, 10.0f, "%.3f"); ImGui::SliderFloat("Gravity (m/s^2)", &s.params.gravity, 0.0f, 30.0f, "%.2f"); ImGui::Separator(); ImGui::TextUnformatted("Rendering"); ImGui::SliderFloat("Soft Depth (m)", &s.params.soft_depth_distance, 0.0f, 2.0f, "%.3f"); if (ImGui::Button("Refresh VFX List")) { refresh_vfx_list(); } ImGui::SameLine(); if (ImGui::Button("Use Flame Defaults")) { s.flipbook_texture = "vfx/flame.ktx2"; s.noise_texture = "vfx/simplex.ktx2"; s.params.flipbook_cols = 16; s.params.flipbook_rows = 4; s.params.flipbook_fps = 30.0f; s.params.flipbook_intensity = 1.0f; s.params.noise_scale = 6.0f; s.params.noise_strength = 0.05f; s.params.noise_scroll = glm::vec2(0.0f, 0.0f); pass->preload_vfx_texture(s.flipbook_texture); pass->preload_vfx_texture(s.noise_texture); } auto combo_vfx = [&](const char *label, std::string &path) { const char *preview = path.empty() ? "None" : path.c_str(); if (ImGui::BeginCombo(label, preview)) { for (const auto &opt : vfxKtx2) { const bool isNone = opt.empty(); const bool isSelected = (path == opt) || (path.empty() && isNone); const char *name = isNone ? "None" : opt.c_str(); if (ImGui::Selectable(name, isSelected)) { path = opt; if (!path.empty()) { pass->preload_vfx_texture(path); } } if (isSelected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } }; ImGui::Separator(); ImGui::TextUnformatted("Flipbook"); combo_vfx("Flipbook Texture", s.flipbook_texture); int cols = (int)s.params.flipbook_cols; int rows = (int)s.params.flipbook_rows; cols = std::max(cols, 1); rows = std::max(rows, 1); if (ImGui::InputInt("Flipbook Cols", &cols)) s.params.flipbook_cols = (uint32_t)std::max(cols, 1); if (ImGui::InputInt("Flipbook Rows", &rows)) s.params.flipbook_rows = (uint32_t)std::max(rows, 1); ImGui::SliderFloat("Flipbook FPS", &s.params.flipbook_fps, 0.0f, 120.0f, "%.1f"); ImGui::SliderFloat("Flipbook Intensity", &s.params.flipbook_intensity, 0.0f, 8.0f, "%.3f"); ImGui::Separator(); ImGui::TextUnformatted("Noise"); combo_vfx("Noise Texture", s.noise_texture); ImGui::SliderFloat("Noise Scale", &s.params.noise_scale, 0.0f, 32.0f, "%.3f"); ImGui::SliderFloat("Noise Strength", &s.params.noise_strength, 0.0f, 1.0f, "%.3f"); ImGui::InputFloat2("Noise Scroll", reinterpret_cast(&s.params.noise_scroll)); ImGui::Separator(); ImGui::TextUnformatted("Color"); ImGui::ColorEdit4("Tint", reinterpret_cast(&s.params.color), ImGuiColorEditFlags_Float); } // 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."); ImGui::Separator(); // Post-processing: FXAA if (auto *fx = eng->_renderPassManager ? eng->_renderPassManager->getPass() : nullptr) { bool fxaaEnabled = fx->enabled(); if (ImGui::Checkbox("FXAA", &fxaaEnabled)) { fx->set_enabled(fxaaEnabled); } float edgeTh = fx->edge_threshold(); if (ImGui::SliderFloat("FXAA Edge Threshold", &edgeTh, 0.01f, 0.5f)) { fx->set_edge_threshold(edgeTh); } float edgeThMin = fx->edge_threshold_min(); if (ImGui::SliderFloat("FXAA Edge Threshold Min", &edgeThMin, 0.0f, 0.1f)) { fx->set_edge_threshold_min(edgeThMin); } } else { ImGui::TextUnformatted("FXAA pass not available"); } ImGui::TextUnformatted("IBL Volumes (reflection probes)"); if (!eng->_iblManager) { ImGui::TextUnformatted("IBLManager not available"); return; } if (eng->_activeIBLVolume < 0) { ImGui::TextUnformatted("Active IBL: Global"); } else { ImGui::Text("Active IBL: Volume %d", eng->_activeIBLVolume); } if (ImGui::Button("Add IBL Volume")) { VulkanEngine::IBLVolume vol{}; if (eng->_sceneManager) { vol.center_world = eng->_sceneManager->getMainCamera().position_world; } vol.halfExtents = glm::vec3(10.0f, 10.0f, 10.0f); vol.paths = eng->_globalIBLPaths; eng->_iblVolumes.push_back(vol); } for (size_t i = 0; i < eng->_iblVolumes.size(); ++i) { auto &vol = eng->_iblVolumes[i]; ImGui::PushID(static_cast(i)); ImGui::Separator(); ImGui::Text("Volume %zu", i); ImGui::SameLine(); if (ImGui::Button("Delete")) { const int idx = static_cast(i); if (eng->_activeIBLVolume == idx) { eng->_activeIBLVolume = -1; } else if (eng->_activeIBLVolume > idx) { eng->_activeIBLVolume -= 1; } if (eng->_pendingIBLRequest.active) { if (eng->_pendingIBLRequest.targetVolume == idx) { eng->_pendingIBLRequest.active = false; } else if (eng->_pendingIBLRequest.targetVolume > idx) { eng->_pendingIBLRequest.targetVolume -= 1; } } eng->_iblVolumes.erase(eng->_iblVolumes.begin() + idx); ImGui::PopID(); break; } ImGui::Checkbox("Enabled", &vol.enabled); { double c[3] = {vol.center_world.x, vol.center_world.y, vol.center_world.z}; if (ImGui::InputScalarN("Center (world)", ImGuiDataType_Double, c, 3, nullptr, nullptr, "%.3f")) { vol.center_world = WorldVec3(c[0], c[1], c[2]); } } ImGui::InputFloat3("Half Extents", &vol.halfExtents.x); // Simple path editors; store absolute or engine-local paths. char specBuf[256]{}; char diffBuf[256]{}; char bgBuf[256]{}; char brdfBuf[256]{}; std::strncpy(specBuf, vol.paths.specularCube.c_str(), sizeof(specBuf) - 1); std::strncpy(diffBuf, vol.paths.diffuseCube.c_str(), sizeof(diffBuf) - 1); std::strncpy(bgBuf, vol.paths.background2D.c_str(), sizeof(bgBuf) - 1); std::strncpy(brdfBuf, vol.paths.brdfLut2D.c_str(), sizeof(brdfBuf) - 1); if (ImGui::InputText("Specular path", specBuf, IM_ARRAYSIZE(specBuf))) { vol.paths.specularCube = specBuf; } if (ImGui::InputText("Diffuse path", diffBuf, IM_ARRAYSIZE(diffBuf))) { vol.paths.diffuseCube = diffBuf; } if (ImGui::InputText("Background path", bgBuf, IM_ARRAYSIZE(bgBuf))) { vol.paths.background2D = bgBuf; } if (ImGui::InputText("BRDF LUT path", brdfBuf, IM_ARRAYSIZE(brdfBuf))) { vol.paths.brdfLut2D = brdfBuf; } if (ImGui::Button("Reload This Volume IBL")) { if (eng->_iblManager && vol.enabled) { vol.paths = resolve_ibl_paths(eng, vol.paths); if (eng->_iblManager->load_async(vol.paths)) { eng->_pendingIBLRequest.active = true; eng->_pendingIBLRequest.targetVolume = static_cast(i); eng->_pendingIBLRequest.paths = vol.paths; } } } ImGui::SameLine(); if (ImGui::Button("Set As Global IBL")) { vol.paths = resolve_ibl_paths(eng, vol.paths); eng->_globalIBLPaths = vol.paths; if (eng->_iblManager) { if (eng->_iblManager->load_async(eng->_globalIBLPaths)) { eng->_pendingIBLRequest.active = true; eng->_pendingIBLRequest.targetVolume = -1; eng->_pendingIBLRequest.paths = eng->_globalIBLPaths; eng->_hasGlobalIBL = false; } } } ImGui::PopID(); } } // 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())); if (eng->_sceneManager) { ImGui::Separator(); WorldVec3 origin = eng->_sceneManager->get_world_origin(); WorldVec3 camWorld = eng->_sceneManager->getMainCamera().position_world; glm::vec3 camLocal = eng->_sceneManager->get_camera_local_position(); ImGui::Text("Origin (world): (%.3f, %.3f, %.3f)", origin.x, origin.y, origin.z); ImGui::Text("Camera (world): (%.3f, %.3f, %.3f)", camWorld.x, camWorld.y, camWorld.z); ImGui::Text("Camera (local): (%.3f, %.3f, %.3f)", camLocal.x, camLocal.y, camLocal.z); } } static void ui_camera(VulkanEngine *eng) { if (!eng || !eng->_sceneManager) { ImGui::TextUnformatted("SceneManager not available"); return; } SceneManager *sceneMgr = eng->_sceneManager.get(); CameraRig &rig = sceneMgr->getCameraRig(); Camera &cam = sceneMgr->getMainCamera(); // Mode switch static const char *k_mode_names[] = {"Free", "Orbit", "Follow", "Chase", "Fixed"}; int mode = static_cast(rig.mode()); if (ImGui::Combo("Mode", &mode, k_mode_names, IM_ARRAYSIZE(k_mode_names))) { rig.set_mode(static_cast(mode), *sceneMgr, cam); if (eng->_input) { eng->_input->set_cursor_mode(CursorMode::Normal); } } ImGui::Text("Active mode: %s", rig.mode_name()); ImGui::Separator(); // Camera state (world) double pos[3] = {cam.position_world.x, cam.position_world.y, cam.position_world.z}; if (ImGui::InputScalarN("Position (world)", ImGuiDataType_Double, pos, 3, nullptr, nullptr, "%.3f")) { cam.position_world = WorldVec3(pos[0], pos[1], pos[2]); } float fov = cam.fovDegrees; if (ImGui::SliderFloat("FOV (deg)", &fov, 30.0f, 110.0f)) { cam.fovDegrees = fov; } WorldVec3 origin = sceneMgr->get_world_origin(); glm::vec3 camLocal = sceneMgr->get_camera_local_position(); ImGui::Text("Origin (world): (%.3f, %.3f, %.3f)", origin.x, origin.y, origin.z); ImGui::Text("Camera (local): (%.3f, %.3f, %.3f)", camLocal.x, camLocal.y, camLocal.z); auto target_from_last_pick = [&](CameraTarget &target) -> bool { PickingSystem *picking = eng->picking(); if (!picking) return false; const auto &pick = picking->last_pick(); if (!pick.valid) return false; if (pick.ownerType == RenderObject::OwnerType::MeshInstance) { target.type = CameraTargetType::MeshInstance; target.name = pick.ownerName; } else if (pick.ownerType == RenderObject::OwnerType::GLTFInstance) { target.type = CameraTargetType::GLTFInstance; target.name = pick.ownerName; } else { target.type = CameraTargetType::WorldPoint; target.world_point = pick.worldPos; target.name.clear(); } return true; }; auto draw_target = [&](const char *id, CameraTarget &target, char *name_buf, size_t name_buf_size) { ImGui::PushID(id); static const char *k_target_types[] = {"None", "WorldPoint", "MeshInstance", "GLTFInstance"}; int type = static_cast(target.type); if (ImGui::Combo("Target type", &type, k_target_types, IM_ARRAYSIZE(k_target_types))) { target.type = static_cast(type); if (target.type != CameraTargetType::MeshInstance && target.type != CameraTargetType::GLTFInstance) { target.name.clear(); if (name_buf_size > 0) { name_buf[0] = '\0'; } } } if (target.type == CameraTargetType::WorldPoint) { double p[3] = {target.world_point.x, target.world_point.y, target.world_point.z}; if (ImGui::InputScalarN("World point", ImGuiDataType_Double, p, 3, nullptr, nullptr, "%.3f")) { target.world_point = WorldVec3(p[0], p[1], p[2]); } } else if (target.type == CameraTargetType::MeshInstance || target.type == CameraTargetType::GLTFInstance) { if (std::strncmp(name_buf, target.name.c_str(), name_buf_size) != 0) { std::snprintf(name_buf, name_buf_size, "%s", target.name.c_str()); } ImGui::InputText("Target name", name_buf, name_buf_size); target.name = name_buf; } WorldVec3 tpos{}; glm::quat trot{}; bool ok = rig.resolve_target(*sceneMgr, target, tpos, trot); ImGui::Text("Resolved: %s", ok ? "yes" : "no"); if (ok) { ImGui::Text("Target world: (%.3f, %.3f, %.3f)", tpos.x, tpos.y, tpos.z); } ImGui::PopID(); }; // Free if (ImGui::CollapsingHeader("Free", ImGuiTreeNodeFlags_DefaultOpen)) { auto &s = rig.free_settings(); ImGui::InputFloat("Move speed (u/s)", &s.move_speed); s.move_speed = std::clamp(s.move_speed, 0.06f, 300.0f); ImGui::InputFloat("Look sensitivity", &s.look_sensitivity); ImGui::InputFloat("Roll speed (rad/s)", &s.roll_speed); ImGui::TextUnformatted("Roll keys: Q/E"); } // Orbit if (ImGui::CollapsingHeader("Orbit")) { auto &s = rig.orbit_settings(); static char orbitName[128] = ""; draw_target("orbit_target", s.target, orbitName, IM_ARRAYSIZE(orbitName)); if (ImGui::Button("Orbit target = Last Pick")) { target_from_last_pick(s.target); } ImGui::InputDouble("Distance", &s.distance, 0.1, 1.0, "%.3f"); s.distance = std::clamp(s.distance, 0.2, 100000.0); float yawDeg = glm::degrees(s.yaw); float pitchDeg = glm::degrees(s.pitch); if (ImGui::SliderFloat("Yaw (deg)", &yawDeg, -180.0f, 180.0f)) { s.yaw = glm::radians(yawDeg); } if (ImGui::SliderFloat("Pitch (deg)", &pitchDeg, -89.0f, 89.0f)) { s.pitch = glm::radians(pitchDeg); } ImGui::InputFloat("Look sensitivity##orbit", &s.look_sensitivity); } // Follow if (ImGui::CollapsingHeader("Follow")) { auto &s = rig.follow_settings(); static char followName[128] = ""; draw_target("follow_target", s.target, followName, IM_ARRAYSIZE(followName)); if (ImGui::Button("Follow target = Last Pick")) { target_from_last_pick(s.target); } ImGui::InputFloat3("Position offset (local)", &s.position_offset_local.x); glm::vec3 rotDeg = glm::degrees(glm::eulerAngles(s.rotation_offset)); float r[3] = {rotDeg.x, rotDeg.y, rotDeg.z}; if (ImGui::InputFloat3("Rotation offset (deg XYZ)", r)) { glm::mat4 R = glm::eulerAngleXYZ(glm::radians(r[0]), glm::radians(r[1]), glm::radians(r[2])); s.rotation_offset = glm::quat_cast(R); } if (ImGui::Button("Reset rotation offset")) { s.rotation_offset = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); } } // Chase if (ImGui::CollapsingHeader("Chase")) { auto &s = rig.chase_settings(); static char chaseName[128] = ""; draw_target("chase_target", s.target, chaseName, IM_ARRAYSIZE(chaseName)); if (ImGui::Button("Chase target = Last Pick")) { target_from_last_pick(s.target); } ImGui::InputFloat3("Position offset (local)##chase", &s.position_offset_local.x); glm::vec3 rotDeg = glm::degrees(glm::eulerAngles(s.rotation_offset)); float r[3] = {rotDeg.x, rotDeg.y, rotDeg.z}; if (ImGui::InputFloat3("Rotation offset (deg XYZ)##chase", r)) { glm::mat4 R = glm::eulerAngleXYZ(glm::radians(r[0]), glm::radians(r[1]), glm::radians(r[2])); s.rotation_offset = glm::quat_cast(R); } ImGui::SliderFloat("Position lag (1/s)", &s.position_lag, 0.0f, 30.0f); ImGui::SliderFloat("Rotation lag (1/s)", &s.rotation_lag, 0.0f, 30.0f); } // Fixed if (ImGui::CollapsingHeader("Fixed")) { ImGui::TextUnformatted("Fixed mode does not modify the camera automatically."); } } // 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(); } } static const char *job_state_name(AsyncAssetLoader::JobState s) { using JS = AsyncAssetLoader::JobState; switch (s) { case JS::Pending: return "Pending"; case JS::Running: return "Running"; case JS::Completed: return "Completed"; case JS::Failed: return "Failed"; case JS::Cancelled: return "Cancelled"; default: return "?"; } } static void ui_async_assets(VulkanEngine *eng) { if (!eng || !eng->_asyncLoader) { ImGui::TextUnformatted("AsyncAssetLoader not available"); return; } std::vector jobs; eng->_asyncLoader->debug_snapshot(jobs); ImGui::Text("Active jobs: %zu", jobs.size()); ImGui::Separator(); if (!jobs.empty()) { if (ImGui::BeginTable("async_jobs", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40); ImGui::TableSetupColumn("Scene"); ImGui::TableSetupColumn("Model"); ImGui::TableSetupColumn("State", ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn("Progress", ImGuiTableColumnFlags_WidthFixed, 180); ImGui::TableHeadersRow(); for (const auto &j : jobs) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Text("%u", j.id); ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(j.scene_name.c_str()); ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(j.model_relative_path.c_str()); ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(job_state_name(j.state)); ImGui::TableSetColumnIndex(4); float p = j.progress; ImGui::ProgressBar(p, ImVec2(-FLT_MIN, 0.0f)); if (j.texture_count > 0) { ImGui::SameLine(); ImGui::Text("(%zu/%zu tex)", j.textures_resident, j.texture_count); } } ImGui::EndTable(); } } else { ImGui::TextUnformatted("No async asset jobs currently running."); } ImGui::Separator(); ImGui::TextUnformatted("Spawn async glTF instance"); static char gltfPath[256] = "mirage2000/scene.gltf"; static char gltfName[128] = "async_gltf_01"; static float gltfPos[3] = {0.0f, 0.0f, 0.0f}; static float gltfRot[3] = {0.0f, 0.0f, 0.0f}; 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("Load glTF async")) { 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->loadGLTFAsync(gltfName, gltfPath, M); eng->preloadInstanceTextures(gltfName); } } // 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; // Global on/off toggle for all shadowing. ImGui::Checkbox("Enable Shadows", &ss.enabled); ImGui::Separator(); ImGui::BeginDisabled(!ss.enabled); 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.enabled && (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::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 (!eng->_context) return; EngineContext *ctx = eng->_context.get(); ImGui::TextUnformatted("Reflections"); bool ssrEnabled = ctx->enableSSR; if (ImGui::Checkbox("Enable Screen-Space Reflections", &ssrEnabled)) { ctx->enableSSR = ssrEnabled; } int reflMode = static_cast(ctx->reflectionMode); ImGui::TextUnformatted("Reflection Mode"); ImGui::RadioButton("SSR only", &reflMode, 0); ImGui::SameLine(); ImGui::RadioButton("SSR + RT fallback", &reflMode, 1); ImGui::SameLine(); ImGui::RadioButton("RT only", &reflMode, 2); const bool rq = eng->_deviceManager->supportsRayQuery(); const bool as = eng->_deviceManager->supportsAccelerationStructure(); if (!(rq && as) && reflMode != 0) { reflMode = 0; // guard for unsupported HW } ctx->reflectionMode = static_cast(reflMode); ImGui::Separator(); ImGui::TextUnformatted("Volumetrics"); bool voxEnabled = ctx->enableVolumetrics; if (ImGui::Checkbox("Enable Voxel Volumetrics (Cloud/Smoke/Flame)", &voxEnabled)) { ctx->enableVolumetrics = voxEnabled; } const char *typeLabels[] = {"Clouds", "Smoke", "Flame"}; for (uint32_t i = 0; i < EngineContext::MAX_VOXEL_VOLUMES; ++i) { VoxelVolumeSettings &vs = ctx->voxelVolumes[i]; std::string header = "Voxel Volume " + std::to_string(i); if (!ImGui::TreeNode(header.c_str())) { continue; } std::string id = "##vox" + std::to_string(i); ImGui::Checkbox(("Enabled" + id).c_str(), &vs.enabled); int type = static_cast(vs.type); if (ImGui::Combo(("Type" + id).c_str(), &type, typeLabels, IM_ARRAYSIZE(typeLabels))) { type = std::clamp(type, 0, 2); vs.type = static_cast(type); } ImGui::Checkbox(("Follow Camera XZ" + id).c_str(), &vs.followCameraXZ); ImGui::Checkbox(("Animate Voxels" + id).c_str(), &vs.animateVoxels); if (vs.followCameraXZ) { ImGui::InputFloat3(("Volume Offset (local)" + id).c_str(), &vs.volumeCenterLocal.x); } else { ImGui::InputFloat3(("Volume Center (local)" + id).c_str(), &vs.volumeCenterLocal.x); } ImGui::InputFloat3(("Volume Velocity (local)" + id).c_str(), &vs.volumeVelocityLocal.x); ImGui::InputFloat3(("Volume Half Extents" + id).c_str(), &vs.volumeHalfExtents.x); vs.volumeHalfExtents.x = std::max(vs.volumeHalfExtents.x, 0.01f); vs.volumeHalfExtents.y = std::max(vs.volumeHalfExtents.y, 0.01f); vs.volumeHalfExtents.z = std::max(vs.volumeHalfExtents.z, 0.01f); ImGui::SliderFloat(("Density Scale" + id).c_str(), &vs.densityScale, 0.0f, 6.0f); ImGui::SliderFloat(("Coverage" + id).c_str(), &vs.coverage, 0.0f, 0.95f); ImGui::SliderFloat(("Extinction" + id).c_str(), &vs.extinction, 0.0f, 8.0f); ImGui::SliderInt(("Steps" + id).c_str(), &vs.stepCount, 8, 256); int gridRes = static_cast(vs.gridResolution); if (ImGui::SliderInt(("Grid Resolution" + id).c_str(), &gridRes, 16, 128)) { vs.gridResolution = static_cast(std::max(4, gridRes)); } if (vs.animateVoxels) { ImGui::InputFloat3(("Wind Velocity (local)" + id).c_str(), &vs.windVelocityLocal.x); ImGui::SliderFloat(("Dissipation" + id).c_str(), &vs.dissipation, 0.0f, 6.0f); ImGui::SliderFloat(("Noise Strength" + id).c_str(), &vs.noiseStrength, 0.0f, 6.0f); ImGui::SliderFloat(("Noise Scale" + id).c_str(), &vs.noiseScale, 0.25f, 32.0f); ImGui::SliderFloat(("Noise Speed" + id).c_str(), &vs.noiseSpeed, 0.0f, 8.0f); if (vs.type != VoxelVolumeType::Clouds) { ImGui::InputFloat3(("Emitter UVW" + id).c_str(), &vs.emitterUVW.x); ImGui::SliderFloat(("Emitter Radius" + id).c_str(), &vs.emitterRadius, 0.01f, 0.5f); } } ImGui::ColorEdit3(("Albedo/Tint" + id).c_str(), &vs.albedo.x); ImGui::SliderFloat(("Scatter Strength" + id).c_str(), &vs.scatterStrength, 0.0f, 2.0f); if (vs.type == VoxelVolumeType::Flame) { ImGui::ColorEdit3(("Emission Color" + id).c_str(), &vs.emissionColor.x); ImGui::SliderFloat(("Emission Strength" + id).c_str(), &vs.emissionStrength, 0.0f, 25.0f); } else { vs.emissionStrength = 0.0f; } ImGui::TreePop(); } ImGui::Separator(); 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); } // Bloom controls bool bloomEnabled = tm->bloomEnabled(); if (ImGui::Checkbox("Bloom", &bloomEnabled)) { tm->setBloomEnabled(bloomEnabled); } float bloomThreshold = tm->bloomThreshold(); if (ImGui::SliderFloat("Bloom Threshold", &bloomThreshold, 0.0f, 5.0f)) { tm->setBloomThreshold(bloomThreshold); } float bloomIntensity = tm->bloomIntensity(); if (ImGui::SliderFloat("Bloom Intensity", &bloomIntensity, 0.0f, 2.0f)) { tm->setBloomIntensity(bloomIntensity); } } 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()); PickingSystem *picking = eng->picking(); if (picking) { bool use_id = picking->use_id_buffer_picking(); if (ImGui::Checkbox("Use ID-buffer picking", &use_id)) { picking->set_use_id_buffer_picking(use_id); } ImGui::Text("Picking mode: %s", use_id ? "ID buffer (async, 1-frame latency)" : "CPU raycast"); bool debug_bvh = picking->debug_draw_bvh(); if (ImGui::Checkbox("Debug draw mesh BVH (last pick)", &debug_bvh)) { picking->set_debug_draw_bvh(debug_bvh); } } else { ImGui::TextUnformatted("Picking system not available"); } // Debug draw settings (engine-owned collector + render pass) if (eng->_context && eng->_context->debug_draw) { DebugDrawSystem *dd = eng->_context->debug_draw; auto &s = dd->settings(); bool enabled = s.enabled; if (ImGui::Checkbox("Enable debug draw", &enabled)) { s.enabled = enabled; } if (s.enabled) { ImGui::SameLine(); ImGui::Text("Commands: %zu", dd->command_count()); int seg = s.segments; if (ImGui::SliderInt("Circle segments", &seg, 3, 128)) { s.segments = seg; } bool depth_tested = s.show_depth_tested; bool overlay = s.show_overlay; if (ImGui::Checkbox("Depth-tested", &depth_tested)) { s.show_depth_tested = depth_tested; } ImGui::SameLine(); if (ImGui::Checkbox("Overlay", &overlay)) { s.show_overlay = overlay; } auto layer_checkbox = [&s](const char *label, DebugDrawLayer layer) { const uint32_t bit = static_cast(layer); bool on = (s.layer_mask & bit) != 0u; if (ImGui::Checkbox(label, &on)) { if (on) s.layer_mask |= bit; else s.layer_mask &= ~bit; } }; ImGui::TextUnformatted("Layers"); layer_checkbox("Physics##dd_layer_physics", DebugDrawLayer::Physics); ImGui::SameLine(); layer_checkbox("Picking##dd_layer_picking", DebugDrawLayer::Picking); ImGui::SameLine(); layer_checkbox("Lights##dd_layer_lights", DebugDrawLayer::Lights); layer_checkbox("Particles##dd_layer_particles", DebugDrawLayer::Particles); ImGui::SameLine(); layer_checkbox("Volumetrics##dd_layer_volumetrics", DebugDrawLayer::Volumetrics); ImGui::SameLine(); layer_checkbox("Misc##dd_layer_misc", DebugDrawLayer::Misc); } } else { ImGui::TextUnformatted("Debug draw system not available"); } 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); } } // Point light editor if (eng->_sceneManager) { ImGui::Separator(); ImGui::TextUnformatted("Point lights"); SceneManager *sceneMgr = eng->_sceneManager.get(); const auto &lights = sceneMgr->getPointLights(); ImGui::Text("Active lights: %zu", lights.size()); static int selectedLight = -1; if (selectedLight >= static_cast(lights.size())) { selectedLight = static_cast(lights.size()) - 1; } if (ImGui::BeginListBox("Light list")) { for (size_t i = 0; i < lights.size(); ++i) { std::string label = fmt::format("Light {}", i); const bool isSelected = (selectedLight == static_cast(i)); if (ImGui::Selectable(label.c_str(), isSelected)) { selectedLight = static_cast(i); } } ImGui::EndListBox(); } // Controls for the selected light if (selectedLight >= 0 && selectedLight < static_cast(lights.size())) { SceneManager::PointLight pl{}; if (sceneMgr->getPointLight(static_cast(selectedLight), pl)) { double pos[3] = {pl.position_world.x, pl.position_world.y, pl.position_world.z}; float col[3] = {pl.color.r, pl.color.g, pl.color.b}; bool changed = false; changed |= ImGui::InputScalarN("Position (world)", ImGuiDataType_Double, pos, 3, nullptr, nullptr, "%.3f"); changed |= ImGui::SliderFloat("Radius", &pl.radius, 0.1f, 1000.0f); changed |= ImGui::ColorEdit3("Color", col); changed |= ImGui::SliderFloat("Intensity", &pl.intensity, 0.0f, 100.0f); if (changed) { pl.position_world = WorldVec3(pos[0], pos[1], pos[2]); pl.color = glm::vec3(col[0], col[1], col[2]); sceneMgr->setPointLight(static_cast(selectedLight), pl); } if (ImGui::Button("Remove selected light")) { sceneMgr->removePointLight(static_cast(selectedLight)); selectedLight = -1; } } } // Controls for adding a new light ImGui::Separator(); ImGui::TextUnformatted("Add point light"); static double newPos[3] = {0.0, 1.0, 0.0}; static float newRadius = 10.0f; static float newColor[3] = {1.0f, 1.0f, 1.0f}; static float newIntensity = 5.0f; ImGui::InputScalarN("New position (world)", ImGuiDataType_Double, newPos, 3, nullptr, nullptr, "%.3f"); ImGui::SliderFloat("New radius", &newRadius, 0.1f, 1000.0f); ImGui::ColorEdit3("New color", newColor); ImGui::SliderFloat("New intensity", &newIntensity, 0.0f, 100.0f); if (ImGui::Button("Add point light")) { SceneManager::PointLight pl{}; pl.position_world = WorldVec3(newPos[0], newPos[1], newPos[2]); pl.radius = newRadius; pl.color = glm::vec3(newColor[0], newColor[1], newColor[2]); pl.intensity = newIntensity; const size_t oldCount = sceneMgr->getPointLightCount(); sceneMgr->addPointLight(pl); selectedLight = static_cast(oldCount); } if (ImGui::Button("Clear all lights")) { sceneMgr->clearPointLights(); selectedLight = -1; } // Spot light editor ImGui::Separator(); ImGui::TextUnformatted("Spot lights"); const auto &spotLights = sceneMgr->getSpotLights(); ImGui::Text("Active spot lights: %zu", spotLights.size()); static int selectedSpot = -1; if (selectedSpot >= static_cast(spotLights.size())) { selectedSpot = static_cast(spotLights.size()) - 1; } if (ImGui::BeginListBox("Spot light list##spot_list")) { for (size_t i = 0; i < spotLights.size(); ++i) { std::string label = fmt::format("Spot {}", i); const bool isSelected = (selectedSpot == static_cast(i)); if (ImGui::Selectable(label.c_str(), isSelected)) { selectedSpot = static_cast(i); } } ImGui::EndListBox(); } if (selectedSpot >= 0 && selectedSpot < static_cast(spotLights.size())) { SceneManager::SpotLight sl{}; if (sceneMgr->getSpotLight(static_cast(selectedSpot), sl)) { double pos[3] = {sl.position_world.x, sl.position_world.y, sl.position_world.z}; float dir[3] = {sl.direction.x, sl.direction.y, sl.direction.z}; float col[3] = {sl.color.r, sl.color.g, sl.color.b}; bool changed = false; changed |= ImGui::InputScalarN("Position (world)##spot_pos", ImGuiDataType_Double, pos, 3, nullptr, nullptr, "%.3f"); changed |= ImGui::InputFloat3("Direction##spot_dir", dir, "%.3f"); changed |= ImGui::SliderFloat("Radius##spot_radius", &sl.radius, 0.1f, 1000.0f); changed |= ImGui::SliderFloat("Inner angle (deg)##spot_inner", &sl.inner_angle_deg, 0.0f, 89.0f); changed |= ImGui::SliderFloat("Outer angle (deg)##spot_outer", &sl.outer_angle_deg, 0.0f, 89.9f); changed |= ImGui::ColorEdit3("Color##spot_color", col); changed |= ImGui::SliderFloat("Intensity##spot_intensity", &sl.intensity, 0.0f, 100.0f); if (changed) { sl.position_world = WorldVec3(pos[0], pos[1], pos[2]); glm::vec3 d{dir[0], dir[1], dir[2]}; sl.direction = (glm::length(d) > 1.0e-6f) ? glm::normalize(d) : glm::vec3(0.0f, -1.0f, 0.0f); sl.color = glm::vec3(col[0], col[1], col[2]); sl.inner_angle_deg = std::clamp(sl.inner_angle_deg, 0.0f, 89.0f); sl.outer_angle_deg = std::clamp(sl.outer_angle_deg, sl.inner_angle_deg, 89.9f); sceneMgr->setSpotLight(static_cast(selectedSpot), sl); } if (ImGui::Button("Remove selected spot light##spot_remove")) { sceneMgr->removeSpotLight(static_cast(selectedSpot)); selectedSpot = -1; } } } ImGui::Separator(); ImGui::TextUnformatted("Add spot light"); static double newSpotPos[3] = {0.0, 2.0, 0.0}; static float newSpotDir[3] = {0.0f, -1.0f, 0.0f}; static float newSpotRadius = 10.0f; static float newSpotInner = 15.0f; static float newSpotOuter = 25.0f; static float newSpotColor[3] = {1.0f, 1.0f, 1.0f}; static float newSpotIntensity = 10.0f; ImGui::InputScalarN("New position (world)##spot_new_pos", ImGuiDataType_Double, newSpotPos, 3, nullptr, nullptr, "%.3f"); ImGui::InputFloat3("New direction##spot_new_dir", newSpotDir, "%.3f"); ImGui::SliderFloat("New radius##spot_new_radius", &newSpotRadius, 0.1f, 1000.0f); ImGui::SliderFloat("New inner angle (deg)##spot_new_inner", &newSpotInner, 0.0f, 89.0f); ImGui::SliderFloat("New outer angle (deg)##spot_new_outer", &newSpotOuter, 0.0f, 89.9f); if (newSpotInner > newSpotOuter) { newSpotOuter = newSpotInner; } ImGui::ColorEdit3("New color##spot_new_color", newSpotColor); ImGui::SliderFloat("New intensity##spot_new_intensity", &newSpotIntensity, 0.0f, 100.0f); if (ImGui::Button("Add spot light##spot_add")) { SceneManager::SpotLight sl{}; sl.position_world = WorldVec3(newSpotPos[0], newSpotPos[1], newSpotPos[2]); glm::vec3 d{newSpotDir[0], newSpotDir[1], newSpotDir[2]}; sl.direction = (glm::length(d) > 1.0e-6f) ? glm::normalize(d) : glm::vec3(0.0f, -1.0f, 0.0f); sl.radius = newSpotRadius; sl.color = glm::vec3(newSpotColor[0], newSpotColor[1], newSpotColor[2]); sl.intensity = newSpotIntensity; sl.inner_angle_deg = std::clamp(newSpotInner, 0.0f, 89.0f); sl.outer_angle_deg = std::clamp(newSpotOuter, sl.inner_angle_deg, 89.9f); const size_t oldCount = sceneMgr->getSpotLightCount(); sceneMgr->addSpotLight(sl); selectedSpot = static_cast(oldCount); } if (ImGui::Button("Clear all spot lights##spot_clear")) { sceneMgr->clearSpotLights(); selectedSpot = -1; } } 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 PickingSystem::PickInfo *pick = nullptr; if (picking) { const auto &last = picking->last_pick(); const auto &hover = picking->hover_pick(); pick = last.valid ? &last : (hover.valid ? &hover : 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); if (ok && picking) { picking->clear_owner_picks(RenderObject::OwnerType::MeshInstance, 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); if (ok) { deleteStatus = "Removed glTF instance: " + pick->ownerName; if (picking) { picking->clear_owner_picks(RenderObject::OwnerType::GLTFInstance, pick->ownerName); } } else { deleteStatus = "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 (picking && picking->last_pick().valid) { const auto &last = picking->last_pick(); const char *meshName = last.mesh ? last.mesh->name.c_str() : ""; const char *sceneName = ""; if (last.scene && !last.scene->debugName.empty()) { sceneName = last.scene->debugName.c_str(); } ImGui::Text("Last pick scene: %s", sceneName); ImGui::Text("Last pick source: %s", picking->use_id_buffer_picking() ? "ID buffer" : "CPU raycast"); ImGui::Text("Last pick object ID: %u", picking->last_pick_object_id()); ImGui::Text("Last pick mesh: %s (surface %u)", meshName, last.surfaceIndex); ImGui::Text("World pos: (%.3f, %.3f, %.3f)", last.worldPos.x, last.worldPos.y, last.worldPos.z); const char *ownerTypeStr = "none"; switch (last.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 = last.ownerName.empty() ? "" : last.ownerName.c_str(); ImGui::Text("Owner: %s (%s)", ownerName, ownerTypeStr); ImGui::Text("Indices: first=%u count=%u", last.firstIndex, last.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 (picking && picking->hover_pick().valid) { const auto &hover = picking->hover_pick(); const char *meshName = hover.mesh ? hover.mesh->name.c_str() : ""; ImGui::Text("Hover mesh: %s (surface %u)", meshName, hover.surfaceIndex); const char *ownerTypeStr = "none"; switch (hover.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 = hover.ownerName.empty() ? "" : hover.ownerName.c_str(); ImGui::Text("Hover owner: %s (%s)", ownerName, ownerTypeStr); } else { ImGui::TextUnformatted("Hover: "); } if (picking && !picking->drag_selection().empty()) { ImGui::Text("Drag selection: %zu objects", picking->drag_selection().size()); } ImGui::Separator(); ImGui::TextUnformatted("Object Gizmo (ImGuizmo)"); if (!eng->_sceneManager) { ImGui::TextUnformatted("SceneManager not available"); return; } SceneManager *sceneMgr = eng->_sceneManager.get(); // Choose a pick to edit: prefer last pick, then hover. PickingSystem::PickInfo *pick = nullptr; if (picking) { if (picking->last_pick().valid) { pick = picking->mutable_last_pick(); } else if (picking->hover_pick().valid) { pick = picking->mutable_hover_pick(); } } if (!pick || pick->ownerName.empty()) { ImGui::TextUnformatted("No selection for gizmo (pick or hover an instance)."); return; } 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 }; GizmoTarget target = GizmoTarget::None; if (pick->ownerType == RenderObject::OwnerType::MeshInstance) { if (sceneMgr->getMeshInstanceTransformLocal(pick->ownerName, targetTransform)) { target = GizmoTarget::MeshInstance; ImGui::Text("Editing mesh instance: %s", pick->ownerName.c_str()); } } else if (pick->ownerType == RenderObject::OwnerType::GLTFInstance) { if (sceneMgr->getGLTFInstanceTransformLocal(pick->ownerName, targetTransform)) { target = GizmoTarget::GLTFInstance; ImGui::Text("Editing glTF instance: %s", pick->ownerName.c_str()); } } if (target == GizmoTarget::None) { ImGui::TextUnformatted("Gizmo only supports dynamic mesh/glTF instances."); return; } ImGuiIO &io = ImGui::GetIO(); ImGuizmo::SetOrthographic(false); VkExtent2D swapExtent = eng->_swapchainManager ? eng->_swapchainManager->swapchainExtent() : VkExtent2D{1, 1}; VkExtent2D drawExtent{ static_cast(static_cast(eng->_logicalRenderExtent.width) * eng->renderScale), static_cast(static_cast(eng->_logicalRenderExtent.height) * eng->renderScale) }; if (drawExtent.width == 0 || drawExtent.height == 0) { drawExtent = VkExtent2D{1, 1}; } VkRect2D activeRect = vkutil::compute_letterbox_rect(drawExtent, swapExtent); const float fbScaleX = (io.DisplayFramebufferScale.x > 0.0f) ? io.DisplayFramebufferScale.x : 1.0f; const float fbScaleY = (io.DisplayFramebufferScale.y > 0.0f) ? io.DisplayFramebufferScale.y : 1.0f; const float rectX = static_cast(activeRect.offset.x) / fbScaleX; const float rectY = static_cast(activeRect.offset.y) / fbScaleY; const float rectW = static_cast(activeRect.extent.width) / fbScaleX; const float rectH = static_cast(activeRect.extent.height) / fbScaleY; ImGuizmo::SetDrawlist(); ImGuizmo::SetRect(rectX, rectY, rectW, rectH); // Build a distance-based perspective projection for ImGuizmo instead of // using the engine's reversed-Z Vulkan projection. Camera &cam = sceneMgr->getMainCamera(); float fovRad = glm::radians(cam.fovDegrees); float aspect = drawExtent.height > 0 ? static_cast(drawExtent.width) / static_cast(drawExtent.height) : 1.0f; // Distance from camera to object; clamp to avoid degenerate planes. glm::vec3 camPos = sceneMgr->get_camera_local_position(); glm::vec3 objPos = glm::vec3(targetTransform[3]); float dist = glm::length(objPos - camPos); if (!std::isfinite(dist) || dist <= 0.0f) { dist = 1.0f; } // Near/far based on distance: keep ratio reasonable for precision. float nearPlane = glm::max(0.05f, dist * 0.05f); float farPlane = glm::max(nearPlane * 50.0f, dist * 2.0f); glm::mat4 view = cam.getViewMatrix(sceneMgr->get_camera_local_position()); glm::mat4 proj = glm::perspective(fovRad, aspect, nearPlane, farPlane); glm::mat4 before = targetTransform; ImDrawList* dl = ImGui::GetForegroundDrawList(); ImGuizmo::SetDrawlist(dl); ImGuizmo::SetRect(rectX, rectY, rectW, rectH); ImGuizmo::Manipulate(&view[0][0], &proj[0][0], op, mode, &targetTransform[0][0]); bool changed = false; for (int c = 0; c < 4 && !changed; ++c) { for (int r = 0; r < 4; ++r) { if (before[c][r] != targetTransform[c][r]) { changed = true; break; } } } if (changed) { switch (target) { case GizmoTarget::MeshInstance: sceneMgr->setMeshInstanceTransformLocal(pick->ownerName, targetTransform); break; case GizmoTarget::GLTFInstance: sceneMgr->setGLTFInstanceTransformLocal(pick->ownerName, targetTransform); break; default: break; } // Keep pick debug info roughly in sync. pick->worldTransform = targetTransform; pick->worldPos = local_to_world(glm::vec3(targetTransform[3]), sceneMgr->get_world_origin()); } } static void ui_planets(VulkanEngine *eng) { if (!eng || !eng->_sceneManager) { return; } SceneManager *scene = eng->_sceneManager.get(); PlanetSystem *planets = scene->get_planet_system(); if (!planets) { ImGui::TextUnformatted("Planet system not available"); return; } bool enabled = planets->enabled(); if (ImGui::Checkbox("Enable planet rendering", &enabled)) { planets->set_enabled(enabled); } const WorldVec3 origin_world = scene->get_world_origin(); const WorldVec3 cam_world = scene->getMainCamera().position_world; const glm::vec3 cam_local = scene->get_camera_local_position(); ImGui::Separator(); ImGui::Text("Camera world (m): %.3f, %.3f, %.3f", cam_world.x, cam_world.y, cam_world.z); ImGui::Text("Camera local (m): %.3f, %.3f, %.3f", cam_local.x, cam_local.y, cam_local.z); ImGui::Text("World origin (m): %.3f, %.3f, %.3f", origin_world.x, origin_world.y, origin_world.z); auto look_at_world = [](Camera &cam, const WorldVec3 &target_world) { glm::dvec3 dirD = glm::normalize(target_world - cam.position_world); glm::vec3 dir = glm::normalize(glm::vec3(dirD)); glm::vec3 up(0.0f, 1.0f, 0.0f); if (glm::length2(glm::cross(dir, up)) < 1e-6f) { up = glm::vec3(0.0f, 0.0f, 1.0f); } glm::vec3 f = dir; glm::vec3 r = glm::normalize(glm::cross(up, f)); glm::vec3 u = glm::cross(f, r); glm::mat3 rot; rot[0] = r; rot[1] = u; rot[2] = -f; // -Z forward cam.orientation = glm::quat_cast(rot); }; PlanetSystem::PlanetBody *earth = planets->get_body(PlanetSystem::BodyID::Earth); PlanetSystem::PlanetBody *moon = planets->get_body(PlanetSystem::BodyID::Moon); if (earth) { ImGui::Separator(); bool vis = earth->visible; if (ImGui::Checkbox("Render Earth", &vis)) { earth->visible = vis; } ImGui::SameLine(); ImGui::Text("(R=%.1f km)", earth->radius_m / 1000.0); const double dist = glm::length(cam_world - earth->center_world); const double alt_m = dist - earth->radius_m; ImGui::Text("Altitude above Earth: %.3f km", alt_m / 1000.0); if (ImGui::Button("Teleport: 10000 km above surface")) { scene->getMainCamera().position_world = earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e7); look_at_world(scene->getMainCamera(), earth->center_world); } if (ImGui::Button("Teleport: 1000 km orbit")) { scene->getMainCamera().position_world = earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e6); look_at_world(scene->getMainCamera(), earth->center_world); } ImGui::SameLine(); if (ImGui::Button("Teleport: 10 km above surface")) { scene->getMainCamera().position_world = earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e4); look_at_world(scene->getMainCamera(), earth->center_world); } } if (moon) { bool vis = moon->visible; if (ImGui::Checkbox("Render Moon", &vis)) { moon->visible = vis; } ImGui::SameLine(); ImGui::Text("(R=%.1f km)", moon->radius_m / 1000.0); } } } // namespace // Window visibility states for menu-bar toggles namespace { struct DebugWindowStates { bool show_overview{false}; bool show_window{false}; bool show_background{false}; bool show_particles{false}; bool show_shadows{false}; bool show_render_graph{false}; bool show_pipelines{false}; bool show_ibl{false}; bool show_postfx{false}; bool show_scene{false}; bool show_camera{false}; bool show_planets{false}; bool show_async_assets{false}; bool show_textures{false}; }; static DebugWindowStates g_debug_windows; } // namespace void vk_engine_draw_debug_ui(VulkanEngine *eng) { if (!eng) return; ImGuizmo::BeginFrame(); // Main menu bar at the top if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Overview", nullptr, &g_debug_windows.show_overview); ImGui::MenuItem("Window", nullptr, &g_debug_windows.show_window); ImGui::Separator(); ImGui::MenuItem("Scene", nullptr, &g_debug_windows.show_scene); ImGui::MenuItem("Camera", nullptr, &g_debug_windows.show_camera); ImGui::MenuItem("Planets", nullptr, &g_debug_windows.show_planets); ImGui::MenuItem("Render Graph", nullptr, &g_debug_windows.show_render_graph); ImGui::MenuItem("Pipelines", nullptr, &g_debug_windows.show_pipelines); ImGui::Separator(); ImGui::MenuItem("Shadows", nullptr, &g_debug_windows.show_shadows); ImGui::MenuItem("IBL", nullptr, &g_debug_windows.show_ibl); ImGui::MenuItem("PostFX", nullptr, &g_debug_windows.show_postfx); ImGui::MenuItem("Background", nullptr, &g_debug_windows.show_background); ImGui::Separator(); ImGui::MenuItem("Particles", nullptr, &g_debug_windows.show_particles); ImGui::MenuItem("Textures", nullptr, &g_debug_windows.show_textures); ImGui::MenuItem("Async Assets", nullptr, &g_debug_windows.show_async_assets); ImGui::EndMenu(); } // Quick stats in menu bar ImGui::Separator(); ImGui::Text("%.1f ms | %d tris | %d draws", eng->stats.frametime, eng->stats.triangle_count, eng->stats.drawcall_count); ImGui::EndMainMenuBar(); } // Individual debug windows (only shown when toggled) if (g_debug_windows.show_overview) { if (ImGui::Begin("Overview", &g_debug_windows.show_overview)) { ui_overview(eng); } ImGui::End(); } if (g_debug_windows.show_window) { if (ImGui::Begin("Window Settings", &g_debug_windows.show_window)) { ui_window(eng); } ImGui::End(); } if (g_debug_windows.show_background) { if (ImGui::Begin("Background", &g_debug_windows.show_background)) { ui_background(eng); } ImGui::End(); } if (g_debug_windows.show_particles) { if (ImGui::Begin("Particles", &g_debug_windows.show_particles)) { ui_particles(eng); } ImGui::End(); } if (g_debug_windows.show_shadows) { if (ImGui::Begin("Shadows", &g_debug_windows.show_shadows)) { ui_shadows(eng); } ImGui::End(); } if (g_debug_windows.show_render_graph) { if (ImGui::Begin("Render Graph", &g_debug_windows.show_render_graph)) { ui_render_graph(eng); } ImGui::End(); } if (g_debug_windows.show_pipelines) { if (ImGui::Begin("Pipelines", &g_debug_windows.show_pipelines)) { ui_pipelines(eng); } ImGui::End(); } if (g_debug_windows.show_ibl) { if (ImGui::Begin("IBL", &g_debug_windows.show_ibl)) { ui_ibl(eng); } ImGui::End(); } if (g_debug_windows.show_postfx) { if (ImGui::Begin("PostFX", &g_debug_windows.show_postfx)) { ui_postfx(eng); } ImGui::End(); } if (g_debug_windows.show_scene) { if (ImGui::Begin("Scene", &g_debug_windows.show_scene)) { ui_scene(eng); } ImGui::End(); } if (g_debug_windows.show_camera) { if (ImGui::Begin("Camera", &g_debug_windows.show_camera)) { ui_camera(eng); } ImGui::End(); } if (g_debug_windows.show_planets) { if (ImGui::Begin("Planets", &g_debug_windows.show_planets)) { ui_planets(eng); } ImGui::End(); } if (g_debug_windows.show_async_assets) { if (ImGui::Begin("Async Assets", &g_debug_windows.show_async_assets)) { ui_async_assets(eng); } ImGui::End(); } if (g_debug_windows.show_textures) { if (ImGui::Begin("Textures", &g_debug_windows.show_textures)) { ui_textures(eng); } ImGui::End(); } }