ADD: Bounding
This commit is contained in:
@@ -49,532 +49,8 @@
|
|||||||
#include "core/texture_cache.h"
|
#include "core/texture_cache.h"
|
||||||
#include "core/ibl_manager.h"
|
#include "core/ibl_manager.h"
|
||||||
|
|
||||||
static size_t query_texture_budget_bytes(DeviceManager* dev)
|
// ImGui debug UI (tabs, inspectors, etc.) is implemented in core/vk_engine_ui.cpp.
|
||||||
{
|
void vk_engine_draw_debug_ui(VulkanEngine *eng);
|
||||||
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<unsigned long long>(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<size_t>(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<BackgroundPass>();
|
|
||||||
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<float *>(&selected.data.data1));
|
|
||||||
ImGui::InputFloat4("data2", reinterpret_cast<float *>(&selected.data.data2));
|
|
||||||
ImGui::InputFloat4("data3", reinterpret_cast<float *>(&selected.data.data3));
|
|
||||||
ImGui::InputFloat4("data4", reinterpret_cast<float *>(&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<Vertex> verts; std::vector<uint32_t> 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<Vertex>(verts.data(), verts.size()),
|
|
||||||
std::span<uint32_t>(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<Vertex>(verts.data(), verts.size()),
|
|
||||||
std::span<uint32_t>(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<Vertex>(verts.data(), verts.size()),
|
|
||||||
std::span<uint32_t>(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 = 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<TextureCache::DebugRow> 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<int>(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<uint32_t>(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<RenderGraph::RGDebugPassInfo> 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<RenderGraph::RGDebugImageInfo> 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<RenderGraph::RGDebugBufferInfo> 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<PipelineManager::GraphicsPipelineDebugInfo> 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<TonemapPass>() : 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() : "<unknown>";
|
|
||||||
const char *sceneName = "<none>";
|
|
||||||
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: <none>");
|
|
||||||
}
|
|
||||||
ImGui::Separator();
|
|
||||||
if (eng->_hoverPick.valid)
|
|
||||||
{
|
|
||||||
const char *meshName = eng->_hoverPick.mesh ? eng->_hoverPick.mesh->name.c_str() : "<unknown>";
|
|
||||||
ImGui::Text("Hover mesh: %s (surface %u)", meshName, eng->_hoverPick.surfaceIndex);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextUnformatted("Hover: <none>");
|
|
||||||
}
|
|
||||||
if (!eng->_dragSelection.empty())
|
|
||||||
{
|
|
||||||
ImGui::Text("Drag selection: %zu objects", eng->_dragSelection.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
VulkanEngine *loadedEngine = nullptr;
|
VulkanEngine *loadedEngine = nullptr;
|
||||||
|
|
||||||
@@ -759,8 +235,8 @@ void VulkanEngine::init()
|
|||||||
// Try to load default IBL assets if present
|
// Try to load default IBL assets if present
|
||||||
{
|
{
|
||||||
IBLPaths ibl{};
|
IBLPaths ibl{};
|
||||||
// ibl.specularCube = _assetManager->assetPath("ibl/docklands.ktx2");
|
ibl.specularCube = _assetManager->assetPath("ibl/docklands.ktx2");
|
||||||
// ibl.diffuseCube = _assetManager->assetPath("ibl/docklands.ktx2"); // temporary: reuse if separate diffuse not provided
|
ibl.diffuseCube = _assetManager->assetPath("ibl/docklands.ktx2"); // temporary: reuse if separate diffuse not provided
|
||||||
ibl.brdfLut2D = _assetManager->assetPath("ibl/brdf_lut.ktx2");
|
ibl.brdfLut2D = _assetManager->assetPath("ibl/brdf_lut.ktx2");
|
||||||
_iblManager->load(ibl);
|
_iblManager->load(ibl);
|
||||||
}
|
}
|
||||||
@@ -1408,61 +884,8 @@ void VulkanEngine::run()
|
|||||||
|
|
||||||
ImGui::NewFrame();
|
ImGui::NewFrame();
|
||||||
|
|
||||||
// Consolidated debug window with tabs
|
// Build the engine debug UI (tabs, inspectors, etc.).
|
||||||
if (ImGui::Begin("Debug"))
|
vk_engine_draw_debug_ui(this);
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Render();
|
ImGui::Render();
|
||||||
draw();
|
draw();
|
||||||
|
|||||||
642
src/core/vk_engine_ui.cpp
Normal file
642
src/core/vk_engine_ui.cpp
Normal file
@@ -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<BackgroundPass>();
|
||||||
|
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<float *>(&selected.data.data1));
|
||||||
|
ImGui::InputFloat4("data2", reinterpret_cast<float *>(&selected.data.data2));
|
||||||
|
ImGui::InputFloat4("data3", reinterpret_cast<float *>(&selected.data.data3));
|
||||||
|
ImGui::InputFloat4("data4", reinterpret_cast<float *>(&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<Vertex> verts;
|
||||||
|
std::vector<uint32_t> 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<Vertex>(verts.data(), verts.size()),
|
||||||
|
std::span<uint32_t>(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<Vertex>(verts.data(), verts.size()),
|
||||||
|
std::span<uint32_t>(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<Vertex>(verts.data(), verts.size()),
|
||||||
|
std::span<uint32_t>(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<TextureCache::DebugRow> 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<int>(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<uint32_t>(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<RenderGraph::RGDebugPassInfo> 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<RenderGraph::RGDebugImageInfo> 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<RenderGraph::RGDebugBufferInfo> 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<PipelineManager::GraphicsPipelineDebugInfo> 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<TonemapPass>() : 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() : "<unknown>";
|
||||||
|
const char *sceneName = "<none>";
|
||||||
|
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: <none>");
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
if (eng->_hoverPick.valid)
|
||||||
|
{
|
||||||
|
const char *meshName = eng->_hoverPick.mesh ? eng->_hoverPick.mesh->name.c_str() : "<unknown>";
|
||||||
|
ImGui::Text("Hover mesh: %s (surface %u)", meshName, eng->_hoverPick.surfaceIndex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted("Hover: <none>");
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
381
src/scene/vk_scene_picking.cpp
Normal file
381
src/scene/vk_scene_picking.cpp
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
#include "vk_scene.h"
|
||||||
|
|
||||||
|
#include "vk_swapchain.h"
|
||||||
|
#include "core/engine_context.h"
|
||||||
|
|
||||||
|
#include "glm/gtx/transform.hpp"
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include "glm/gtx/norm.inl"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
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<float>::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<glm::vec3, 8> 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<float>(extent.width);
|
||||||
|
float height = static_cast<float>(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<float>::max();
|
||||||
|
glm::vec3 bestHitPos{};
|
||||||
|
|
||||||
|
auto testList = [&](const std::vector<RenderObject> &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<RenderObject> &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<RenderObject> &outObjects) const
|
||||||
|
{
|
||||||
|
if (!_context || !_context->getSwapchain())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkExtent2D extent = _context->getSwapchain()->windowExtent();
|
||||||
|
if (extent.width == 0 || extent.height == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float width = static_cast<float>(extent.width);
|
||||||
|
float height = static_cast<float>(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<RenderObject> &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);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user