diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 32b05a4..2e02fc9 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -33,6 +33,7 @@ set(SRC_FILES "src/Graphics/Swapchain.cpp" "src/Graphics/Util.cpp" "src/Graphics/RenderResources.cpp" + "src/Graphics/ResourcesInspector.cpp" "src/Graphics/Managers/VulkanInstanceManager.cpp" "src/Graphics/Managers/MemoryManager.cpp" diff --git a/destrum/include/destrum/App.h b/destrum/include/destrum/App.h index ff3c73a..28789c9 100644 --- a/destrum/include/destrum/App.h +++ b/destrum/include/destrum/App.h @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -41,6 +42,9 @@ public: virtual void onWindowResize(int, int) {}; + // Called inside the "Windows" menu of the main menu bar, add toggles for the game's own windows here + virtual void customWindowsMenu() {} + protected: SDL_Window* window{nullptr}; AppParams m_params{}; @@ -72,6 +76,17 @@ protected: float m_PhysicsTimeScale = 1.0f; int m_PhysicsStepsLastFrame = 0; + struct DebugWindows { + bool stats{true}; + bool physics{true}; + bool inspector{true}; + bool imageCache{false}; + }; + + DebugWindows m_DebugWindows{}; + ResourcesInspector m_ResourcesInspector; + + void drawWindowsMenu(); void drawPhysicsPanel(float dt, float fixedDt, float accumulator); }; diff --git a/destrum/include/destrum/Graphics/Pipelines/ImguiPass.h b/destrum/include/destrum/Graphics/Pipelines/ImguiPass.h index c3b4b97..513b494 100644 --- a/destrum/include/destrum/Graphics/Pipelines/ImguiPass.h +++ b/destrum/include/destrum/Graphics/Pipelines/ImguiPass.h @@ -1,8 +1,15 @@ #ifndef DESTRUM_IMGUI_PASS_H #define DESTRUM_IMGUI_PASS_H +#include +#include +#include + #include #include +#include + +#include class GfxDevice; @@ -33,9 +40,19 @@ public: [[nodiscard]] bool wantsMouse() const; [[nodiscard]] bool wantsKeyboard() const; + // Wraps an image view for ImGui::Image in the frame that's being built. Call it every frame + // you draw the image, the descriptor set is freed again once the GPU is done with that frame. + // The image has to be in imageLayout by the time the UI gets rendered at the end of the frame. + [[nodiscard]] ImTextureID getFrameTexture( + VkImageView imageView, + VkImageLayout imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + private: void initVulkanBackend(); + void releaseTextures(std::vector& textures); + void releaseAllTextures(); + void transitionToColorAttachment( VkCommandBuffer cmd, VkImage image, @@ -50,6 +67,17 @@ private: VkDescriptorPool descriptorPool = VK_NULL_HANDLE; VkFormat colorFormat = VK_FORMAT_UNDEFINED; + struct FrameTexture { + VkImageView imageView{VK_NULL_HANDLE}; + VkImageLayout imageLayout{VK_IMAGE_LAYOUT_UNDEFINED}; + VkDescriptorSet descriptorSet{VK_NULL_HANDLE}; + }; + + // created while building the current UI, not submitted yet + std::vector pendingTextures; + // submitted with a frame, freed once that frame's fence has been waited on + std::array, FRAMES_IN_FLIGHT> inFlightTextures; + bool initialized = false; bool frameBegun = false; bool contextCreated = false; diff --git a/destrum/include/destrum/Graphics/RenderResources.h b/destrum/include/destrum/Graphics/RenderResources.h index 7638681..a9c893c 100644 --- a/destrum/include/destrum/Graphics/RenderResources.h +++ b/destrum/include/destrum/Graphics/RenderResources.h @@ -53,6 +53,7 @@ public: [[nodiscard]] ImageID getWhiteTextureID() const { return whiteImageId; } [[nodiscard]] ImageID getErrorTextureID() const { return errorImageId; } + [[nodiscard]] ImageID getDefaultNormalTextureID() const { return defaultNormalImageId; } BindlessSetManager& getBindlessSetManager(); VkDescriptorSetLayout getBindlessDescSetLayout() const; @@ -62,6 +63,10 @@ public: VkPipelineLayout layout, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS) const; + [[nodiscard]] bool isInitialized() const { return imageCache != nullptr && materialCache != nullptr; } + + const ImageCache& images() const { return *imageCache; } + MeshCache& meshes() { return *meshCache; } const MeshCache& meshes() const { return *meshCache; } diff --git a/destrum/include/destrum/Graphics/ResourcesInspector.h b/destrum/include/destrum/Graphics/ResourcesInspector.h new file mode 100644 index 0000000..08efad6 --- /dev/null +++ b/destrum/include/destrum/Graphics/ResourcesInspector.h @@ -0,0 +1,35 @@ +#ifndef RESOURCESINSPECTOR_H +#define RESOURCESINSPECTOR_H + +#include + +struct GPUImage; +class ImguiPass; +class RenderResources; + +// ImGui windows for looking into the render resource caches +class ResourcesInspector final +{ +public: + void drawImageCacheWindow(bool* open, const RenderResources& resources, ImguiPass& imguiPass); + +private: + struct PreviewInfo + { + bool previewable{false}; + const char* reason{""}; + }; + + [[nodiscard]] static PreviewInfo getPreviewInfo(const GPUImage& image); + + void drawImagePreview(const GPUImage& image, float maxSize, ImguiPass& imguiPass) const; + void drawImageDetails(ImageID id, const RenderResources& resources) const; + [[nodiscard]] bool matchesFilter(ImageID id, const RenderResources& resources) const; + + char filter[128]{}; + float thumbnailSize{48.0f}; + bool onlyPreviewable{false}; + ImageID selectedImageId{NULL_IMAGE_ID}; +}; + +#endif //RESOURCESINSPECTOR_H diff --git a/destrum/include/destrum/Scene/Scene.h b/destrum/include/destrum/Scene/Scene.h index 756107f..556a8b9 100644 --- a/destrum/include/destrum/Scene/Scene.h +++ b/destrum/include/destrum/Scene/Scene.h @@ -33,7 +33,7 @@ public: void FixedUpdate(float dt); void LateUpdate(float dt); void Render(const RenderContext& ctx); - void RenderImgui(); + void RenderImgui(bool* open = nullptr); void CleanupDestroyedGameObjects(); void Unload(); diff --git a/destrum/include/destrum/Scene/SceneManager.h b/destrum/include/destrum/Scene/SceneManager.h index 7a0c8ba..a81e56d 100644 --- a/destrum/include/destrum/Scene/SceneManager.h +++ b/destrum/include/destrum/Scene/SceneManager.h @@ -23,7 +23,7 @@ public: void LateUpdate(float dt); void Render(const RenderContext& ctx); - void RenderImgui(); + void RenderImgui(bool* open = nullptr); void HandleGameObjectDestroy(); void DestroyGameObjects(); diff --git a/destrum/include/destrum/Util/ImGuiUtils.h b/destrum/include/destrum/Util/ImGuiUtils.h index 96e4d83..4191ecb 100644 --- a/destrum/include/destrum/Util/ImGuiUtils.h +++ b/destrum/include/destrum/Util/ImGuiUtils.h @@ -21,7 +21,8 @@ namespace ImGuiUtils { std::string_view sceneName, const std::vector>& objects, const std::vector>& pendingAdditions, - ObjectId& selectedObjectId); + ObjectId& selectedObjectId, + bool* open = nullptr); } #endif // DESTRUM_IMGUIUTILS_H diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index 2aa36d5..d9a4e9a 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -195,13 +195,31 @@ if (!isRunning) break; if (m_params.showDebugUi) { ZoneScopedN("Debug ImGui"); - SceneManager::GetInstance().RenderImgui(); + drawWindowsMenu(); - ImGui::Begin("Debug"); - ImGui::Text("FPS: %.2f", avgFPS); - ImGui::End(); + if (m_DebugWindows.inspector) + { + SceneManager::GetInstance().RenderImgui(&m_DebugWindows.inspector); + } - drawPhysicsPanel(dt, fixedDt, accumulator); + if (m_DebugWindows.stats) + { + if (ImGui::Begin("Debug", &m_DebugWindows.stats)) + { + ImGui::Text("FPS: %.2f", avgFPS); + } + ImGui::End(); + } + + if (m_DebugWindows.physics) + { + drawPhysicsPanel(dt, fixedDt, accumulator); + } + + if (m_DebugWindows.imageCache) + { + m_ResourcesInspector.drawImageCacheWindow(&m_DebugWindows.imageCache, resources, imguiPass); + } TracyPlot("FPS", avgFPS); TracyPlot("Frame Time ms", dt * 1000.0f); @@ -359,9 +377,36 @@ void App::cleanup() } } +void App::drawWindowsMenu() +{ + // ImGui appends to the main menu bar, so this ends up next to whatever menus the game added + if (!ImGui::BeginMainMenuBar()) + { + return; + } + + if (ImGui::BeginMenu("Windows")) + { + ImGui::MenuItem("Debug", nullptr, &m_DebugWindows.stats); + ImGui::MenuItem("Physics", nullptr, &m_DebugWindows.physics); + ImGui::MenuItem("Inspector", nullptr, &m_DebugWindows.inspector); + ImGui::MenuItem("Image Cache", nullptr, &m_DebugWindows.imageCache); + + customWindowsMenu(); + + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); +} + void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator) { - ImGui::Begin("Physics"); + if (!ImGui::Begin("Physics", &m_DebugWindows.physics)) + { + ImGui::End(); + return; + } ImGui::Text("Frame dt: %.3f ms", dt * 1000.0f); ImGui::Text("Fixed dt: %.3f ms", fixedDt * 1000.0f); diff --git a/destrum/src/Graphics/Pipelines/ImguiPass.cpp b/destrum/src/Graphics/Pipelines/ImguiPass.cpp index 3977eb3..4cc98b4 100644 --- a/destrum/src/Graphics/Pipelines/ImguiPass.cpp +++ b/destrum/src/Graphics/Pipelines/ImguiPass.cpp @@ -122,6 +122,15 @@ void ImguiPass::beginFrame() { return; } + // the previous UI was built but never rendered (e.g. the frame got skipped while resizing), + // so the GPU never saw these + std::vector unusedTextures; + for (const FrameTexture& texture : pendingTextures) { + unusedTextures.push_back(texture.descriptorSet); + } + pendingTextures.clear(); + releaseTextures(unusedTextures); + ImGui_ImplVulkan_NewFrame(); ImGui_ImplSDL2_NewFrame(); @@ -155,6 +164,18 @@ void ImguiPass::render( return; } + { + // beginFrame on the gfx device already waited for this frame index' fence, + // so the textures it used last time around can go + auto& inFlight = inFlightTextures[gfx->getCurrentFrameIndex() % FRAMES_IN_FLIGHT]; + releaseTextures(inFlight); + + for (const FrameTexture& texture : pendingTextures) { + inFlight.push_back(texture.descriptorSet); + } + pendingTextures.clear(); + } + ImDrawData* drawData = ImGui::GetDrawData(); if (drawData == nullptr || drawData->CmdListsCount == 0) { return; @@ -200,6 +221,8 @@ void ImguiPass::onSwapchainRecreated() { const auto newFormat = gfx->getSwapchainFormat(); if (newFormat != colorFormat) { if (vulkanInitialized) { + // the swapchain recreate waited for the device, nothing is in flight anymore + releaseAllTextures(); ImGui_ImplVulkan_Shutdown(); vulkanInitialized = false; } @@ -224,6 +247,7 @@ void ImguiPass::cleanup() { } if (vulkanInitialized) { + releaseAllTextures(); ImGui_ImplVulkan_Shutdown(); vulkanInitialized = false; } @@ -293,3 +317,49 @@ void ImguiPass::transitionToColorAttachment( vkCmdPipelineBarrier2(cmd, &dependencyInfo); } + +ImTextureID ImguiPass::getFrameTexture(VkImageView imageView, VkImageLayout imageLayout) { + if (!vulkanInitialized || imageView == VK_NULL_HANDLE) { + return ImTextureID{}; + } + + for (const FrameTexture& texture : pendingTextures) { + if (texture.imageView == imageView && texture.imageLayout == imageLayout) { + return reinterpret_cast(texture.descriptorSet); + } + } + + const VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(imageView, imageLayout); + if (descriptorSet == VK_NULL_HANDLE) { + return ImTextureID{}; + } + + pendingTextures.push_back(FrameTexture{ + .imageView = imageView, + .imageLayout = imageLayout, + .descriptorSet = descriptorSet, + }); + return reinterpret_cast(descriptorSet); +} + +void ImguiPass::releaseTextures(std::vector& textures) { + if (vulkanInitialized) { + for (VkDescriptorSet descriptorSet : textures) { + ImGui_ImplVulkan_RemoveTexture(descriptorSet); + } + } + textures.clear(); +} + +void ImguiPass::releaseAllTextures() { + std::vector textures; + for (const FrameTexture& texture : pendingTextures) { + textures.push_back(texture.descriptorSet); + } + pendingTextures.clear(); + releaseTextures(textures); + + for (auto& inFlight : inFlightTextures) { + releaseTextures(inFlight); + } +} diff --git a/destrum/src/Graphics/Resources/Cubemap.cpp b/destrum/src/Graphics/Resources/Cubemap.cpp index 6c4ab05..73b0f6c 100644 --- a/destrum/src/Graphics/Resources/Cubemap.cpp +++ b/destrum/src/Graphics/Resources/Cubemap.cpp @@ -205,7 +205,8 @@ void CubeMap::CreateCubeMap( .mipMap = false, .isCubemap = true }); - vkutil::addDebugLabel(gfxDevice.getDevice(), cubeMapImage.image, "skybox cubemap"); + cubeMapImage.debugName = "skybox cubemap"; + vkutil::addDebugLabel(gfxDevice.getDevice(), cubeMapImage.image, cubeMapImage.debugName.c_str()); std::array faceViews{}; diff --git a/destrum/src/Graphics/ResourcesInspector.cpp b/destrum/src/Graphics/ResourcesInspector.cpp new file mode 100644 index 0000000..7ee29d5 --- /dev/null +++ b/destrum/src/Graphics/ResourcesInspector.cpp @@ -0,0 +1,439 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace +{ + [[nodiscard]] const char* StripPrefix(const char* name, const char* prefix) + { + const std::size_t length = std::strlen(prefix); + return std::strncmp(name, prefix, length) == 0 ? name + length : name; + } + + [[nodiscard]] const char* FormatName(VkFormat format) + { + return StripPrefix(string_VkFormat(format), "VK_FORMAT_"); + } + + [[nodiscard]] const char* LayoutName(VkImageLayout layout) + { + return StripPrefix(string_VkImageLayout(layout), "VK_IMAGE_LAYOUT_"); + } + + [[nodiscard]] std::string UsageNames(VkImageUsageFlags usage) + { + struct UsageName + { + VkImageUsageFlagBits bit; + const char* name; + }; + + constexpr UsageName usageNames[] = { + {VK_IMAGE_USAGE_SAMPLED_BIT, "sampled"}, + {VK_IMAGE_USAGE_STORAGE_BIT, "storage"}, + {VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, "color attachment"}, + {VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, "depth attachment"}, + {VK_IMAGE_USAGE_TRANSFER_SRC_BIT, "transfer src"}, + {VK_IMAGE_USAGE_TRANSFER_DST_BIT, "transfer dst"}, + }; + + std::string result; + for (const UsageName& usageName : usageNames) + { + if ((usage & usageName.bit) == 0) + { + continue; + } + if (!result.empty()) + { + result += ", "; + } + result += usageName.name; + } + return result.empty() ? "none" : result; + } + + [[nodiscard]] bool IsDepthFormat(VkFormat format) + { + switch (format) + { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D32_SFLOAT: + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return true; + default: + return false; + } + } + + [[nodiscard]] bool ContainsCaseInsensitive(std::string_view haystack, std::string_view needle) + { + const auto it = std::search( + haystack.begin(), haystack.end(), + needle.begin(), needle.end(), + [](char a, char b) + { + return std::tolower(static_cast(a)) == std::tolower(static_cast(b)); + }); + return it != haystack.end(); + } + + [[nodiscard]] const char* TextureIntentName(TextureIntent intent) + { + switch (intent) + { + case TextureIntent::ColorSrgb: return "color (sRGB)"; + case TextureIntent::DataLinear: return "data (linear)"; + } + return "unknown"; + } +} + +ResourcesInspector::PreviewInfo ResourcesInspector::getPreviewInfo(const GPUImage& image) +{ + if (image.image == VK_NULL_HANDLE || image.imageView == VK_NULL_HANDLE) + { + return {false, "no image view"}; + } + if ((image.usage & VK_IMAGE_USAGE_SAMPLED_BIT) == 0) + { + return {false, "not sampleable"}; + } + if (image.isCubemap) + { + return {false, "cubemap"}; + } + if (image.numLayers > 1) + { + return {false, "image array"}; + } + + if (image.getLayout() == VK_IMAGE_LAYOUT_UNDEFINED) + { + return {false, "never written"}; + } + + // ImGui reads the image when the UI gets rendered at the very end of the frame. Render targets + // like the draw / depth image are in an attachment or transfer layout by then. + if (image.getLayout() != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) + { + return {false, "render target"}; + } + + return {true, ""}; +} + +void ResourcesInspector::drawImagePreview(const GPUImage& image, float maxSize, ImguiPass& imguiPass) const +{ + const float width = static_cast(image.extent.width); + const float height = static_cast(image.extent.height); + const float scale = maxSize / std::max({width, height, 1.0f}); + const ImVec2 size{std::max(width * scale, 1.0f), std::max(height * scale, 1.0f)}; + + const PreviewInfo preview = getPreviewInfo(image); + if (preview.previewable) + { + const ImTextureID textureId = imguiPass.getFrameTexture(image.imageView); + if (textureId != ImTextureID{}) + { + ImGui::ImageWithBg(textureId, size, ImVec2{0.0f, 0.0f}, ImVec2{1.0f, 1.0f}, ImVec4{0.1f, 0.1f, 0.1f, 1.0f}); + return; + } + } + + // placeholder box with the reason it can't be shown + const ImVec2 boxSize{maxSize, std::min(maxSize, std::max(size.y, ImGui::GetTextLineHeight() * 2.0f))}; + const ImVec2 min = ImGui::GetCursorScreenPos(); + const ImVec2 max{min.x + boxSize.x, min.y + boxSize.y}; + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(min, max, ImGui::GetColorU32(ImGuiCol_FrameBg)); + drawList->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border)); + + // small thumbnails just get a dash, the reason is in the tooltip + const char* text = ImGui::CalcTextSize(preview.reason).x <= boxSize.x - 8.0f ? preview.reason : "-"; + const ImVec2 textSize = ImGui::CalcTextSize(text); + drawList->AddText( + ImVec2{min.x + (boxSize.x - textSize.x) * 0.5f, min.y + (boxSize.y - textSize.y) * 0.5f}, + ImGui::GetColorU32(ImGuiCol_TextDisabled), + text); + + ImGui::Dummy(boxSize); +} + +void ResourcesInspector::drawImageDetails(ImageID id, const RenderResources& resources) const +{ + const ImageCache& cache = resources.images(); + const GPUImage& image = cache.images[id]; + + ImGui::Text("Image ID: %u", static_cast(id)); + ImGui::TextWrapped("Name: %s", image.debugName.empty() ? "" : image.debugName.c_str()); + + if (id == resources.getWhiteTextureID()) + { + ImGui::TextColored(ImVec4{0.6f, 0.8f, 1.0f, 1.0f}, "Default white (also metallic/roughness + emissive default)"); + } + if (id == resources.getErrorTextureID()) + { + ImGui::TextColored(ImVec4{1.0f, 0.4f, 0.8f, 1.0f}, "Error texture (used when a load fails)"); + } + if (id == resources.getDefaultNormalTextureID()) + { + ImGui::TextColored(ImVec4{0.6f, 0.8f, 1.0f, 1.0f}, "Default normal map"); + } + + ImGui::Separator(); + + ImGui::Text("Size: %u x %u", image.extent.width, image.extent.height); + ImGui::Text("Format: %s%s", FormatName(image.format), IsDepthFormat(image.format) ? " (shown as red)" : ""); + ImGui::Text("Mips: %u Layers: %u%s", image.mipLevels, image.numLayers, image.isCubemap ? " cubemap" : ""); + ImGui::Text("Layout: %s", LayoutName(image.getLayout())); + if (const PreviewInfo preview = getPreviewInfo(image); !preview.previewable) + { + ImGui::TextDisabled("No preview: %s", preview.reason); + } + ImGui::TextWrapped("Usage: %s", UsageNames(image.usage).c_str()); + + if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end()) + { + ImGui::Separator(); + ImGui::TextWrapped("File: %s", it->second.path.generic_string().c_str()); + ImGui::Text("Intent: %s%s", TextureIntentName(it->second.intent), it->second.mipMap ? ", mipmapped" : ""); + } + + const MaterialCache& materials = resources.materials(); + std::vector users; + for (std::size_t materialId = 0; materialId < materials.materials.size(); ++materialId) + { + if (materials.materials[materialId].diffuseTexture == id) + { + users.push_back(materialId); + } + } + + if (!users.empty()) + { + ImGui::Separator(); + ImGui::Text("Diffuse texture of %zu material(s):", users.size()); + + constexpr std::size_t maxListed = 8; + for (std::size_t i = 0; i < std::min(users.size(), maxListed); ++i) + { + const Material& material = materials.materials[users[i]]; + ImGui::BulletText( + "#%zu %s", + users[i], + material.name.empty() ? "" : material.name.c_str()); + } + if (users.size() > maxListed) + { + ImGui::TextDisabled("... and %zu more", users.size() - maxListed); + } + } +} + +bool ResourcesInspector::matchesFilter(ImageID id, const RenderResources& resources) const +{ + const std::string_view filterText{filter}; + if (filterText.empty()) + { + return true; + } + + // a plain number matches the image id exactly + unsigned filterId = 0; + const auto [end, error] = std::from_chars(filterText.data(), filterText.data() + filterText.size(), filterId); + if (error == std::errc{} && end == filterText.data() + filterText.size()) + { + return filterId == id; + } + + const ImageCache& cache = resources.images(); + if (ContainsCaseInsensitive(cache.images[id].debugName, filterText)) + { + return true; + } + + if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end()) + { + return ContainsCaseInsensitive(it->second.path.generic_string(), filterText); + } + return false; +} + +void ResourcesInspector::drawImageCacheWindow(bool* open, const RenderResources& resources, ImguiPass& imguiPass) +{ + if (!ImGui::Begin("Image Cache", open)) + { + ImGui::End(); + return; + } + + if (!resources.isInitialized()) + { + ImGui::TextDisabled("Render resources aren't initialized yet"); + ImGui::End(); + return; + } + + const ImageCache& cache = resources.images(); + + std::vector visibleIds; + std::size_t imageCount = 0; + for (std::size_t i = 0; i < cache.images.size(); ++i) + { + const auto id = static_cast(i); + const GPUImage& image = cache.images[i]; + if (image.image == VK_NULL_HANDLE) + { + continue; + } + + ++imageCount; + if (!matchesFilter(id, resources)) + { + continue; + } + if (onlyPreviewable && !getPreviewInfo(image).previewable) + { + continue; + } + visibleIds.push_back(id); + } + + ImGui::Text("%zu images (bindless capacity %u)", imageCount, cache.getMaxImageCount()); + + ImGui::SetNextItemWidth(220.0f); + ImGui::InputTextWithHint("##filter", "Filter by name, path or ID", filter, sizeof(filter)); + ImGui::SameLine(); + ImGui::SetNextItemWidth(140.0f); + ImGui::SliderFloat("Thumbnail", &thumbnailSize, 24.0f, 256.0f, "%.0f px"); + ImGui::SameLine(); + ImGui::Checkbox("Previewable only", &onlyPreviewable); + + if (selectedImageId != NULL_IMAGE_ID && + (selectedImageId >= cache.images.size() || cache.images[selectedImageId].image == VK_NULL_HANDLE)) + { + selectedImageId = NULL_IMAGE_ID; + } + + if (selectedImageId != NULL_IMAGE_ID) + { + if (ImGui::CollapsingHeader("Selected Image", ImGuiTreeNodeFlags_DefaultOpen)) + { + drawImagePreview(cache.images[selectedImageId], 256.0f, imguiPass); + ImGui::SameLine(); + ImGui::BeginGroup(); + drawImageDetails(selectedImageId, resources); + if (ImGui::SmallButton("Clear selection")) + { + selectedImageId = NULL_IMAGE_ID; + } + ImGui::EndGroup(); + } + } + + constexpr ImGuiTableFlags tableFlags = + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY | + ImGuiTableFlags_Resizable | + ImGuiTableFlags_SizingStretchProp; + + if (ImGui::BeginTable("##images", 6, tableFlags)) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 40.0f); + ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthFixed, thumbnailSize + 4.0f); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 3.0f); + ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("Format", ImGuiTableColumnFlags_WidthStretch, 1.5f); + ImGui::TableSetupColumn("Layout", ImGuiTableColumnFlags_WidthStretch, 1.5f); + ImGui::TableHeadersRow(); + + // only the visible rows get a texture descriptor + const float rowHeight = thumbnailSize + ImGui::GetStyle().CellPadding.y * 2.0f; + ImGuiListClipper clipper; + clipper.Begin(static_cast(visibleIds.size()), rowHeight); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + const ImageID id = visibleIds[static_cast(row)]; + const GPUImage& image = cache.images[id]; + + ImGui::PushID(static_cast(id)); + ImGui::TableNextRow(ImGuiTableRowFlags_None, thumbnailSize); + + ImGui::TableNextColumn(); + const std::string idLabel = std::to_string(id); + if (ImGui::Selectable( + idLabel.c_str(), + selectedImageId == id, + ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, + ImVec2{0.0f, thumbnailSize})) + { + selectedImageId = selectedImageId == id ? NULL_IMAGE_ID : id; + } + + if (ImGui::BeginItemTooltip()) + { + drawImagePreview(image, 256.0f, imguiPass); + drawImageDetails(id, resources); + ImGui::EndTooltip(); + } + + ImGui::TableNextColumn(); + drawImagePreview(image, thumbnailSize, imguiPass); + + ImGui::TableNextColumn(); + if (const auto it = cache.loadedImagesInfo.find(id); it != cache.loadedImagesInfo.end()) + { + // loaded images are named after their full path, the file name is enough here + ImGui::TextWrapped("%s", it->second.path.filename().generic_string().c_str()); + } + else + { + ImGui::TextWrapped("%s", image.debugName.empty() ? "" : image.debugName.c_str()); + } + + ImGui::TableNextColumn(); + ImGui::Text("%u x %u", image.extent.width, image.extent.height); + if (image.mipLevels > 1) + { + ImGui::TextDisabled("%u mips", image.mipLevels); + } + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(FormatName(image.format)); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(LayoutName(image.getLayout())); + + ImGui::PopID(); + } + } + + ImGui::EndTable(); + } + + ImGui::End(); +} diff --git a/destrum/src/Scene/Scene.cpp b/destrum/src/Scene/Scene.cpp index bea7b92..79afb2b 100644 --- a/destrum/src/Scene/Scene.cpp +++ b/destrum/src/Scene/Scene.cpp @@ -164,12 +164,13 @@ void Scene::Render(const RenderContext& ctx) { } -void Scene::RenderImgui() { +void Scene::RenderImgui(bool* open) { ImGuiUtils::RenderSceneInspector( m_name, m_objects, m_pendingAdditions, - m_SelectedObjectId); + m_SelectedObjectId, + open); } void Scene::CleanupDestroyedGameObjects() { diff --git a/destrum/src/Scene/SceneManager.cpp b/destrum/src/Scene/SceneManager.cpp index f7222c7..8d7d48d 100644 --- a/destrum/src/Scene/SceneManager.cpp +++ b/destrum/src/Scene/SceneManager.cpp @@ -38,9 +38,9 @@ void SceneManager::Render(const RenderContext& ctx) { } } -void SceneManager::RenderImgui() { +void SceneManager::RenderImgui(bool* open) { for (const auto& scene : m_activeScenes) { - scene->RenderImgui(); + scene->RenderImgui(open); } } diff --git a/destrum/src/Util/ImGuiUtils.cpp b/destrum/src/Util/ImGuiUtils.cpp index 854ce18..d406304 100644 --- a/destrum/src/Util/ImGuiUtils.cpp +++ b/destrum/src/Util/ImGuiUtils.cpp @@ -123,7 +123,8 @@ namespace ImGuiUtils { std::string_view sceneName, const std::vector>& objectsSource, const std::vector>& pendingAdditions, - ObjectId& selectedObjectId) { + ObjectId& selectedObjectId, + bool* open) { RegisterEngineComponents(); std::vector objects; @@ -154,7 +155,7 @@ namespace ImGuiUtils { } ImGui::SetNextWindowSize(ImVec2(640.0f, 520.0f), ImGuiCond_FirstUseEver); - if (!ImGui::Begin("Inspector")) { + if (!ImGui::Begin("Inspector", open)) { ImGui::End(); return; } diff --git a/destrum/third_party/CMakeLists.txt b/destrum/third_party/CMakeLists.txt index 4f23f46..e72f2e0 100644 --- a/destrum/third_party/CMakeLists.txt +++ b/destrum/third_party/CMakeLists.txt @@ -64,8 +64,32 @@ set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(ASSIMP_INJECT_DEBUG_POSTFIX OFF CACHE BOOL "" FORCE) set(ASSIMP_INSTALL OFF CACHE BOOL "" FORCE) set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE BOOL "" FORCE) + +# assimp is unbearably slow unoptimized (a big FBX takes ~10s), so build it optimized even in Debug. +# This only affects targets created inside the assimp directory (including its bundled zlib). +set(DESTRUM_SAVED_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") +set(DESTRUM_SAVED_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") +if (MSVC) + foreach (flags_var CMAKE_C_FLAGS_DEBUG CMAKE_CXX_FLAGS_DEBUG) + string(REPLACE "/RTC1" "" ${flags_var} "${${flags_var}}") + string(REPLACE "/Od" "/O2" ${flags_var} "${${flags_var}}") + string(REPLACE "/Ob0" "/Ob2" ${flags_var} "${${flags_var}}") + endforeach () +else () + string(APPEND CMAKE_C_FLAGS_DEBUG " -O2") + string(APPEND CMAKE_CXX_FLAGS_DEBUG " -O2") +endif () + add_subdirectory(assimp) +# assimp's own CMakeLists appends "/Od" to the Debug flags again, target options come later and win +if (MSVC) + target_compile_options(assimp PRIVATE $<$:/O2>) +endif () + +set(CMAKE_C_FLAGS_DEBUG "${DESTRUM_SAVED_C_FLAGS_DEBUG}") +set(CMAKE_CXX_FLAGS_DEBUG "${DESTRUM_SAVED_CXX_FLAGS_DEBUG}") + set(IMGUI_DIR "${CMAKE_CURRENT_LIST_DIR}/imgui") add_library(imgui STATIC diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index b6725e6..684cb9d 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -22,6 +22,7 @@ public: void customFixedUpdate(float dt) override; void onWindowResize(int newWidth, int newHeight) override; + void customWindowsMenu() override; private: void drawDebugMenuBar(); @@ -49,6 +50,7 @@ private: bool renderBoxColliders{false}; bool renderQuads{true}; bool renderDebugLines{true}; + bool showTestWindow{true}; }; #endif //LIGHTKEEPER_H diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 44e9740..04a437d 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -539,8 +539,8 @@ void LightKeeper::customUpdate(float dt) renderer.setRenderWireframe(!renderer.getRenderWireframe()); } - if (m_params.showDebugUi) { - ImGui::Begin("Test"); + if (m_params.showDebugUi && showTestWindow) { + ImGui::Begin("Test", &showTestWindow); if (ImGui::Button("SPawn ball")) { auto sphere = SceneManager::GetInstance().GetCurrentScene().CreateGameObject("Sphere"); @@ -563,6 +563,12 @@ void LightKeeper::customUpdate(float dt) } +void LightKeeper::customWindowsMenu() +{ + ImGui::Separator(); + ImGui::MenuItem("Test", nullptr, &showTestWindow); +} + void LightKeeper::drawDebugMenuBar() { if (!ImGui::BeginMainMenuBar()) {