diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 31a89a6..afdba72 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -3,10 +3,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include @@ -27,7 +30,7 @@ class Application { Application(const Application&) = delete; Application& operator=(const Application&) = delete; - Application(const Config& config); + explicit Application(const Config& config); virtual ~Application(); void run(); @@ -40,15 +43,15 @@ class Application { virtual void onRender() {} virtual void onDestroy() {} + AssetRegistry& assets() { return this->assetRegistry; } + PanelRegistry& panels() { return this->panelRegistry; } + + DebugUI& debugUI() { return this->_debugUI; } + Canvas canvas; Camera2D camera; std::vector args; - // Creates a mesh that persists across frames, for callers that want to - // draw the same geometry many times with different transforms (e.g. - // instancing) instead of resubmitting vertices every frame via Canvas. - MeshHandle createMesh(std::vector vertices, - std::vector indices); void drawMesh(MeshHandle handle, const glm::mat4& transform); private: @@ -59,6 +62,11 @@ class Application { class Impl; std::unique_ptr impl; + + AssetRegistry assetRegistry; + PanelRegistry panelRegistry; + + DebugUI _debugUI; }; } // namespace sophia diff --git a/engine/include/sophia/ui/debug_ui.hpp b/engine/include/sophia/ui/debug_ui.hpp new file mode 100644 index 0000000..45e78fd --- /dev/null +++ b/engine/include/sophia/ui/debug_ui.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace sophia { + +class DebugUI { + public: + DebugUI(const DebugUI&) = delete; + DebugUI& operator=(const DebugUI&) = delete; + + explicit DebugUI(PanelRegistry& registry); + ~DebugUI() = default; + + void show(); + void hide(); + + void showPanelRegistry(bool show = true); + void showFrameStats(bool show = true); + + private: + PanelRegistry& registry; + + PanelHandle panelRegistryHandle; + PanelHandle frameStatsHandle; +}; + +} // namespace sophia diff --git a/engine/include/sophia/ui/panel.hpp b/engine/include/sophia/ui/panel.hpp new file mode 100644 index 0000000..ef81a54 --- /dev/null +++ b/engine/include/sophia/ui/panel.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace sophia { + +class Panel { + public: + Panel(const Panel&) = delete; + Panel& operator=(const Panel&) = delete; + + Panel(Panel&&) noexcept = default; + Panel& operator=(Panel&&) noexcept = default; + + explicit Panel(std::string name = {}) : name{std::move(name)} {} + virtual ~Panel() = default; + + virtual void onUpdate(const FrameInfo& frameInfo) = 0; + + const std::string& getName() const { return name; } + + bool isVisible() const { return this->visible; } + void setVisible(bool visible) { this->visible = visible; } + void toggleVisible() { this->visible = !this->visible; } + + protected: + bool visible = true; + + private: + std::string name; +}; + +} // namespace sophia diff --git a/engine/include/sophia/ui/panel_registry.hpp b/engine/include/sophia/ui/panel_registry.hpp new file mode 100644 index 0000000..9c425ed --- /dev/null +++ b/engine/include/sophia/ui/panel_registry.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sophia { + +class PanelRegistry { + public: + PanelRegistry(const PanelRegistry&) = delete; + PanelRegistry& operator=(const PanelRegistry&) = delete; + + PanelRegistry() = default; + ~PanelRegistry() = default; + + PanelHandle add(std::unique_ptr panel); + + template + PanelHandle emplace(Args&&... args) { + return add(std::make_unique(std::forward(args)...)); + } + + const Panel& get(PanelHandle handle) const; + Panel& get(PanelHandle handle) { + return const_cast(std::as_const(*this).get(handle)); + } + + template + const T& get(PanelHandle handle) const { + const Panel& panel = get(handle); + SOPHIA_ASSERT(dynamic_cast(&panel) != nullptr, + "PanelHandle does not refer to a", typeid(T).name()); + return static_cast(panel); + } + + template + T& get(PanelHandle handle) { + return const_cast(std::as_const(*this).template get(handle)); + } + + void remove(PanelHandle handle); + bool isValid(PanelHandle handle) const; + + template + const T* find() const { + for (const Slot& slot : slots) { + if (slot.panel) { + if (const T* panel = dynamic_cast(slot.panel.get())) { + return panel; + } + } + } + return nullptr; + } + + template + T* find() { + return const_cast(std::as_const(*this).template find()); + } + + const Panel* find(std::string_view name) const { + for (const Slot& slot : slots) { + if (slot.panel && slot.panel->getName() == name) { + return slot.panel.get(); + } + } + return nullptr; + } + + Panel* find(std::string_view name) { + return const_cast(std::as_const(*this).find(name)); + } + + u32 size() const; + u32 visibleCount() const; + + std::vector names() const { + std::vector result; + result.reserve(slots.size()); + for (const Slot& slot : slots) { + if (slot.panel) { + result.push_back(slot.panel->getName()); + } + } + return result; + } + + void update(const FrameInfo& frameInfo); + + private: + struct Slot { + std::unique_ptr panel; + u32 generation = 0; + }; + + std::vector slots; + std::vector freeList; +}; + +} // namespace sophia diff --git a/engine/src/imgui_renderer.cpp b/engine/src/imgui_renderer.cpp new file mode 100644 index 0000000..f0e2f04 --- /dev/null +++ b/engine/src/imgui_renderer.cpp @@ -0,0 +1,85 @@ +#include "imgui_renderer.hpp" + +#include +#include +#include + +namespace sophia { + +ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, + vk::Format colorAttachmentFormat, + u32 minImageCount) { + // 1. Construct ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + ImGui_ImplGlfw_InitForVulkan(window.getGLFWwindow(), true); + + ImGui_ImplVulkan_InitInfo initInfo = {}; + device.populateImGuiInitInfo(initInfo); + initInfo.DescriptorPoolSize = + IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE + 5; + initInfo.MinImageCount = minImageCount; + initInfo.ImageCount = minImageCount; + + VkFormat rawColorAttachmentFormat = + static_cast(colorAttachmentFormat); + initInfo.UseDynamicRendering = true; + initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.sType = + VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR; + initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = + 1; + initInfo.PipelineInfoMain.PipelineRenderingCreateInfo + .pColorAttachmentFormats = &rawColorAttachmentFormat; + + ImGui_ImplVulkan_Init(&initInfo); + + // 2. Enable dark theme + ImGui::StyleColorsDark(); + + // 3. Enable docking / keyboard + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + /** + * When viewports are enabled we tweak WindowRounding/WindowBg so + * platform windows can look identical to regular ones. + */ + ImGuiStyle& style = ImGui::GetStyle(); + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + style.WindowRounding = 0.0f; + style.Colors[ImGuiCol_WindowBg].w = 1.0f; + } +} + +ImGuiRenderer::~ImGuiRenderer() { + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); +} + +void ImGuiRenderer::beginFrame() { + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), + ImGuiDockNodeFlags_PassthruCentralNode); +} + +void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) { + ImGui::Render(); + ImDrawData* mainDrawData = ImGui::GetDrawData(); + + ImGuiIO& io = ImGui::GetIO(); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + ImGui::UpdatePlatformWindows(); + ImGui::RenderPlatformWindowsDefault(); + } + + ImGui_ImplVulkan_RenderDrawData(mainDrawData, commandBuffer); +} + +} // namespace sophia diff --git a/engine/src/imgui_renderer.hpp b/engine/src/imgui_renderer.hpp new file mode 100644 index 0000000..0016730 --- /dev/null +++ b/engine/src/imgui_renderer.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "device.hpp" +#include "window.hpp" + +namespace sophia { + +class ImGuiRenderer { + public: + ImGuiRenderer(const ImGuiRenderer&) = delete; + ImGuiRenderer& operator=(const ImGuiRenderer&) = delete; + + ImGuiRenderer(Window& window, Device& device, + vk::Format colorAttachmentFormat, u32 minImageCount); + ~ImGuiRenderer(); + + void beginFrame(); + void endFrame(vk::CommandBuffer commandBuffer); +}; + +} // namespace sophia diff --git a/engine/src/render_context.hpp b/engine/src/render_context.hpp index a7be9cb..34e7ac1 100644 --- a/engine/src/render_context.hpp +++ b/engine/src/render_context.hpp @@ -45,6 +45,9 @@ class RenderContext { vk::Format getImageFormat() const { return this->swapchain->getImageFormat(); } + u32 getImageCount() const { + return static_cast(this->swapchain->imageCount()); + } private: void recreateSwapchain(); diff --git a/engine/src/ui/debug_ui.cpp b/engine/src/ui/debug_ui.cpp new file mode 100644 index 0000000..2803ea4 --- /dev/null +++ b/engine/src/ui/debug_ui.cpp @@ -0,0 +1,37 @@ +#include + +#include "panels/frame_stats_panel.hpp" +#include "panels/panel_registry_panel.hpp" + +namespace sophia { + +DebugUI::DebugUI(PanelRegistry& registry) : registry{registry} { + // #ifndef NDEBUG + this->panelRegistryHandle = registry.emplace(registry); + this->frameStatsHandle = registry.emplace(); + // #endif // !NDEBUG +} + +void DebugUI::show() { + showPanelRegistry(true); + showFrameStats(true); +} + +void DebugUI::hide() { + showPanelRegistry(false); + showFrameStats(false); +} + +void DebugUI::showPanelRegistry(bool show) { + if (registry.isValid(panelRegistryHandle)) { + registry.get(panelRegistryHandle).setVisible(show); + } +} + +void DebugUI::showFrameStats(bool show) { + if (registry.isValid(frameStatsHandle)) { + registry.get(frameStatsHandle).setVisible(show); + } +} + +} // namespace sophia diff --git a/engine/src/ui/panel_registry.cpp b/engine/src/ui/panel_registry.cpp new file mode 100644 index 0000000..92b58d6 --- /dev/null +++ b/engine/src/ui/panel_registry.cpp @@ -0,0 +1,88 @@ +#include +#include + +namespace sophia { + +namespace { +constexpr u32 GENERATION_MASK = (1u << 12) - 1; +} // namespace + +PanelHandle PanelRegistry::add(std::unique_ptr panel) { + if (!freeList.empty()) { + u32 index = freeList.back(); + freeList.pop_back(); + slots[index].panel = std::move(panel); + return PanelHandle{index, slots[index].generation}; + } + + slots.push_back(Slot{std::move(panel), 0}); + return PanelHandle{static_cast(slots.size() - 1), 0}; +} + +const Panel& PanelRegistry::get(PanelHandle handle) const { + SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle"); + SOPHIA_ASSERT(handle.index < slots.size(), + "PanelHandle index out of range:", handle.index); + + const Slot& slot = slots[handle.index]; + SOPHIA_ASSERT(slot.generation == handle.generation, + "stale PanelHandle (generation mismatch):", handle.index); + SOPHIA_ASSERT(slot.panel != nullptr, + "PanelHandle refers to a removed panel:", handle.index); + return *slot.panel; +} + +void PanelRegistry::remove(PanelHandle handle) { + SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle"); + SOPHIA_ASSERT(handle.index < slots.size(), + "PanelHandle index out of range:", handle.index); + + Slot& slot = slots[handle.index]; + SOPHIA_ASSERT(slot.generation == handle.generation, + "stale PanelHandle (generation mismatch):", handle.index); + SOPHIA_ASSERT(slot.panel != nullptr, + "PanelHandle already removed:", handle.index); + + slot.panel.reset(); + slot.generation = (slot.generation + 1) & GENERATION_MASK; + freeList.push_back(handle.index); +} + +bool PanelRegistry::isValid(PanelHandle handle) const { + if (!handle.isValid() || handle.index >= slots.size()) { + return false; + } + + const Slot& slot = slots[handle.index]; + return slot.generation == handle.generation && slot.panel != nullptr; +} + +u32 PanelRegistry::size() const { + u32 count = 0; + for (const Slot& slot : slots) { + if (slot.panel) { + count++; + } + } + return count; +} + +u32 PanelRegistry::visibleCount() const { + u32 count = 0; + for (const Slot& slot : slots) { + if (slot.panel && slot.panel->isVisible()) { + count++; + } + } + return count; +} + +void PanelRegistry::update(const FrameInfo& frameInfo) { + for (auto& slot : slots) { + if (slot.panel && slot.panel->isVisible()) { + slot.panel->onUpdate(frameInfo); + } + } +} + +} // namespace sophia diff --git a/engine/src/ui/panels/ecs_inspector_panel.cpp b/engine/src/ui/panels/ecs_inspector_panel.cpp new file mode 100644 index 0000000..49e504e --- /dev/null +++ b/engine/src/ui/panels/ecs_inspector_panel.cpp @@ -0,0 +1,57 @@ +#include "ecs_inspector_panel.hpp" + +#include + +#include + +namespace sophia { + +void EcsInspectorPanel::onUpdate(const FrameInfo&) { + ImGui::Begin("ECS Inspector"); + + entityScratch.clear(); + for (entt::entity entity : registry.storage()) { + entityScratch.push_back(entity); + } + + ImGui::BeginChild("EntityList", ImVec2(0, 200), true); + ImGuiListClipper clipper; + clipper.Begin(static_cast(entityScratch.size())); + while (clipper.Step()) { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { + entt::entity entity = entityScratch[static_cast(i)]; + const TagComponent* tag = registry.try_get(entity); + std::string label = tag ? tag->tag : "Entity"; + + ImGui::PushID(static_cast(entt::to_integral(entity))); + if (ImGui::Selectable(label.c_str(), selected == entity)) { + selected = entity; + } + ImGui::PopID(); + } + } + ImGui::EndChild(); + + ImGui::Separator(); + + if (registry.valid(selected)) { + if (auto* transform = registry.try_get(selected)) { + ImGui::DragFloat3("Position", &transform->position.x, 0.1f); + ImGui::DragFloat3("Scale", &transform->scale.x, 0.1f); + ImGui::Text("Rotation: %.2f, %.2f, %.2f, %.2f", transform->rotation.w, + transform->rotation.x, transform->rotation.y, + transform->rotation.z); + } + + if (auto* mesh = registry.try_get(selected)) { + ImGui::Text("Mesh: index=%u generation=%u", mesh->handle.index, + mesh->handle.generation); + } + } else { + ImGui::TextDisabled("No entity selected"); + } + + ImGui::End(); +} + +} // namespace sophia diff --git a/engine/src/ui/panels/ecs_inspector_panel.hpp b/engine/src/ui/panels/ecs_inspector_panel.hpp new file mode 100644 index 0000000..1666ff5 --- /dev/null +++ b/engine/src/ui/panels/ecs_inspector_panel.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +namespace sophia { + +class EcsInspectorPanel : public Panel { + public: + explicit EcsInspectorPanel(entt::registry& registry) + : Panel{"ECS Inspector"}, registry{registry} {} + + void onUpdate(const FrameInfo& frameInfo) override; + + private: + entt::registry& registry; + entt::entity selected = entt::null; + std::vector entityScratch; +}; + +} // namespace sophia diff --git a/engine/src/ui/panels/frame_stats_panel.hpp b/engine/src/ui/panels/frame_stats_panel.hpp new file mode 100644 index 0000000..0d7917f --- /dev/null +++ b/engine/src/ui/panels/frame_stats_panel.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include +#include + +namespace sophia { + +class FrameStatsPanel : public Panel { + public: + FrameStatsPanel() : Panel{"Frame Stats"} {} + + void onUpdate(const FrameInfo& frameInfo) override { + frameCount++; + accumulatedTime += frameInfo.deltaTime; + + if (frameInfo.elapsedTime >= nextRefreshTime) { + displayedFps = static_cast(frameCount) / accumulatedTime; + displayedFrameTimeMs = + (accumulatedTime / static_cast(frameCount)) * 1000.0; + + frameCount = 0; + accumulatedTime = 0.0; + nextRefreshTime = frameInfo.elapsedTime + REFRESH_INTERVAL; + } + + ImGui::Begin("Frame Stats"); + ImGui::Text("FPS: %.1f", displayedFps); + ImGui::Text("Frame time: %.3f ms", displayedFrameTimeMs); + ImGui::End(); + } + + private: + static constexpr double REFRESH_INTERVAL = 1.0; + + double accumulatedTime = 0.0; + u32 frameCount = 0; + double nextRefreshTime = 0.0; + double displayedFps = 0.0; + double displayedFrameTimeMs = 0.0; +}; + +} // namespace sophia diff --git a/engine/src/ui/panels/panel_registry_panel.cpp b/engine/src/ui/panels/panel_registry_panel.cpp new file mode 100644 index 0000000..c481b5a --- /dev/null +++ b/engine/src/ui/panels/panel_registry_panel.cpp @@ -0,0 +1,28 @@ +#include "panel_registry_panel.hpp" + +#include + +namespace sophia { + +void PanelRegistryPanel::onUpdate(const FrameInfo&) { + ImGui::Begin("PanelRegistry"); + ImGui::Text("%u panels (%u visible)", registry.size(), + registry.visibleCount()); + ImGui::Separator(); + + for (std::string_view name : registry.names()) { + Panel* panel = registry.find(name); + if (!panel || panel == this) { + continue; + } + + bool visible = panel->isVisible(); + if (ImGui::Checkbox(name.data(), &visible)) { + panel->setVisible(visible); + } + } + + ImGui::End(); +} + +} // namespace sophia diff --git a/engine/src/ui/panels/panel_registry_panel.hpp b/engine/src/ui/panels/panel_registry_panel.hpp new file mode 100644 index 0000000..29e16e2 --- /dev/null +++ b/engine/src/ui/panels/panel_registry_panel.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace sophia { + +class PanelRegistryPanel : public Panel { + public: + explicit PanelRegistryPanel(PanelRegistry& registry) + : Panel{"PanelRegistry"}, registry{registry} {} + + void onUpdate(const FrameInfo& frameInfo) override; + + private: + PanelRegistry& registry; +}; + +} // namespace sophia