From ec8cc170d3af8a3d6ebc95aa7d18ad5202ed0273 Mon Sep 17 00:00:00 2001 From: gituser12981u2 Date: Wed, 28 Jan 2026 10:52:32 -0800 Subject: [PATCH 1/2] feat: clean up logging code and make it display agnostic --- src/backend/profiling/logging/CMakeLists.txt | 2 +- ...{profiling_logger.cpp => console_sink.cpp} | 161 ++++++++---------- .../profiling/logging/console_sink.hpp | 15 ++ .../profiling/logging/profiling_logger.hpp | 88 ---------- .../profiling/profilers/gpu_frame_stats.hpp | 12 ++ .../profiling/profilers/vk_gpu_profiler.cpp | 6 +- .../profiling/profilers/vk_gpu_profiler.hpp | 16 +- src/backend/profiling/telemetry/publish.cpp | 45 ++++- src/backend/profiling/telemetry/publish.hpp | 7 +- src/backend/profiling/telemetry/telemetry.hpp | 3 + .../profiling/view/profiler_snapshot.cpp | 39 +++++ .../profiling/view/profiler_snapshot.hpp | 22 +++ src/engine/app/app.cpp | 7 + src/engine/app/app.hpp | 6 +- src/engine/editor/editor_ui.hpp | 0 src/engine/editor/imgui/imgui_context.hpp | 0 src/engine/editor/panels/profiler_panel.hpp | 0 src/engine/jobs/job_system.cpp | 1 - src/engine/jobs/job_system.hpp | 1 - src/render/CMakeLists.txt | 2 + src/render/renderer.cpp | 31 ++-- src/render/renderer.hpp | 3 - 22 files changed, 244 insertions(+), 223 deletions(-) rename src/backend/profiling/logging/{profiling_logger.cpp => console_sink.cpp} (62%) create mode 100644 src/backend/profiling/logging/console_sink.hpp delete mode 100644 src/backend/profiling/logging/profiling_logger.hpp create mode 100644 src/backend/profiling/profilers/gpu_frame_stats.hpp create mode 100644 src/backend/profiling/view/profiler_snapshot.cpp create mode 100644 src/backend/profiling/view/profiler_snapshot.hpp create mode 100644 src/engine/editor/editor_ui.hpp create mode 100644 src/engine/editor/imgui/imgui_context.hpp create mode 100644 src/engine/editor/panels/profiler_panel.hpp diff --git a/src/backend/profiling/logging/CMakeLists.txt b/src/backend/profiling/logging/CMakeLists.txt index 267ddc7..3ce215f 100644 --- a/src/backend/profiling/logging/CMakeLists.txt +++ b/src/backend/profiling/logging/CMakeLists.txt @@ -1,5 +1,5 @@ add_library(quark_backend_profiling_logging STATIC - profiling_logger.cpp + console_sink.cpp ) target_include_directories(quark_backend_profiling_logging diff --git a/src/backend/profiling/logging/profiling_logger.cpp b/src/backend/profiling/logging/console_sink.cpp similarity index 62% rename from src/backend/profiling/logging/profiling_logger.cpp rename to src/backend/profiling/logging/console_sink.cpp index 7634c00..3e1a216 100644 --- a/src/backend/profiling/logging/profiling_logger.cpp +++ b/src/backend/profiling/logging/console_sink.cpp @@ -1,8 +1,11 @@ -#include "backend/profiling/logging/profiling_logger.hpp" +#include "backend/profiling/logging/console_sink.hpp" +#if defined(ENABLE_TELEMETRY) #include "backend/profiling/profilers/cpu_profiler.hpp" +#include "backend/profiling/profilers/gpu_frame_stats.hpp" #include "backend/profiling/profilers/upload_profiler.hpp" -#include "backend/profiling/profilers/vk_gpu_profiler.hpp" +#include "backend/profiling/telemetry/publish.hpp" +#include "backend/profiling/telemetry/telemetry.hpp" #include #include @@ -39,15 +42,6 @@ static inline void formatBytes(char *out, std::size_t outSize, out[outSize - 1] = '\0'; } -bool FrameLogger::shouldLog() noexcept { - if (m_period == 0) { - return false; - } - - ++m_frameCounter; - return (m_frameCounter % m_period) == 0ULL; -} - static inline double msAt(const CpuProfiler::Frame &st, CpuProfiler::Stat stat) noexcept { return st.ms[static_cast(stat)]; @@ -67,9 +61,7 @@ static inline void formatMs(char *out, size_t outSize, double ms) noexcept { } } -static void logCpu(const CpuProfiler &cpu) noexcept { - const auto &st = cpu.last(); - +static void logCpuFrame(const CpuProfiler::Frame &cpu) noexcept { std::array line1{}; std::array line2{}; @@ -82,16 +74,18 @@ static void logCpu(const CpuProfiler &cpu) noexcept { std::array pres{}; std::array other{}; - formatMs(frame.data(), frame.size(), msAt(st, CpuProfiler::Stat::FrameTotal)); - formatMs(acq.data(), acq.size(), msAt(st, CpuProfiler::Stat::Acquire)); + formatMs(frame.data(), frame.size(), + msAt(cpu, CpuProfiler::Stat::FrameTotal)); + formatMs(acq.data(), acq.size(), msAt(cpu, CpuProfiler::Stat::Acquire)); formatMs(fence.data(), fence.size(), - msAt(st, CpuProfiler::Stat::WaitForFence)); + msAt(cpu, CpuProfiler::Stat::WaitForFence)); formatMs(ubo.data(), ubo.size(), - msAt(st, CpuProfiler::Stat::UpdatePerFrameUBO)); - formatMs(rec.data(), rec.size(), msAt(st, CpuProfiler::Stat::RecordCmd)); - formatMs(sub.data(), sub.size(), msAt(st, CpuProfiler::Stat::QueueSubmit)); - formatMs(pres.data(), pres.size(), msAt(st, CpuProfiler::Stat::QueuePresent)); - formatMs(other.data(), other.size(), msAt(st, CpuProfiler::Stat::Other)); + msAt(cpu, CpuProfiler::Stat::UpdatePerFrameUBO)); + formatMs(rec.data(), rec.size(), msAt(cpu, CpuProfiler::Stat::RecordCmd)); + formatMs(sub.data(), sub.size(), msAt(cpu, CpuProfiler::Stat::QueueSubmit)); + formatMs(pres.data(), pres.size(), + msAt(cpu, CpuProfiler::Stat::QueuePresent)); + formatMs(other.data(), other.size(), msAt(cpu, CpuProfiler::Stat::Other)); ignore_snprintf(std::snprintf( line1.data(), line1.size(), @@ -103,88 +97,65 @@ static void logCpu(const CpuProfiler &cpu) noexcept { ignore_snprintf(std::snprintf( line2.data(), line2.size(), "CPU cnt: draws %-6u inst %-6u tris %-8llu pipe %-4u desc %-4u", - st.drawCalls, st.instances, static_cast(st.triangles), - st.pipelineBinds, st.descriptorBinds)); - - std::cerr << "\n[Profiler]\n" << line1.data() << "\n" << line2.data() << "\n"; -} - -static void logGpu(const VkGpuProfiler &gpu) noexcept { - const auto &gst = gpu.last(); - if (!gst.valid) { - return; - } - - std::array frame{}; - std::array main{}; - std::array idle{}; + cpu.drawCalls, cpu.instances, + static_cast(cpu.triangles), cpu.pipelineBinds, + cpu.descriptorBinds)); - formatMs(frame.data(), frame.size(), gst.frameMs); - formatMs(main.data(), main.size(), gst.mainPassMs); - formatMs(idle.data(), idle.size(), gst.idleGapMs); - - std::array line{}; - ignore_snprintf(std::snprintf(line.data(), line.size(), - "GPU ms: frame %s main %s idle %s", - frame.data(), main.data(), idle.data())); - - std::cerr << line.data() << "\n"; + std::cerr << line1.data() << "\n" << line2.data() << "\n"; } -static void logUpload(const UploadProfiler &upload) noexcept { - const auto &ust = upload.last(); - const auto < = upload.lifetime(); - +static void logUploadFrame(const UploadProfiler::Frame &upload, + const UploadProfiler::Frame &lifetime) noexcept { const auto idx = [](UploadProfiler::Stat stat) { return static_cast(stat); }; // per frame const std::uint64_t submitCount = - ust.v[idx(UploadProfiler::Stat::UploadSubmitCount)]; + upload.v[idx(UploadProfiler::Stat::UploadSubmitCount)]; const std::uint64_t memcpyCount = - ust.v[idx(UploadProfiler::Stat::UploadMemcpyCount)]; + upload.v[idx(UploadProfiler::Stat::UploadMemcpyCount)]; const std::uint64_t memcpyBytes = - ust.v[idx(UploadProfiler::Stat::UploadMemcpyBytes)]; + upload.v[idx(UploadProfiler::Stat::UploadMemcpyBytes)]; const std::uint64_t stagingUsedBytes = - ust.v[idx(UploadProfiler::Stat::StagingUsedBytes)]; + upload.v[idx(UploadProfiler::Stat::StagingUsedBytes)]; const std::uint64_t bufCount = - ust.v[idx(UploadProfiler::Stat::BufferUploadCount)]; + upload.v[idx(UploadProfiler::Stat::BufferUploadCount)]; const std::uint64_t bufBytes = - ust.v[idx(UploadProfiler::Stat::BufferUploadBytes)]; + upload.v[idx(UploadProfiler::Stat::BufferUploadBytes)]; const std::uint64_t texCount = - ust.v[idx(UploadProfiler::Stat::TextureUploadCount)]; + upload.v[idx(UploadProfiler::Stat::TextureUploadCount)]; const std::uint64_t texBytes = - ust.v[idx(UploadProfiler::Stat::TextureUploadBytes)]; + upload.v[idx(UploadProfiler::Stat::TextureUploadBytes)]; const std::uint64_t matCount = - ust.v[idx(UploadProfiler::Stat::MaterialUploadCount)]; + upload.v[idx(UploadProfiler::Stat::MaterialUploadCount)]; const std::uint64_t matBytes = - ust.v[idx(UploadProfiler::Stat::MaterialUploadBytes)]; + upload.v[idx(UploadProfiler::Stat::MaterialUploadBytes)]; const std::uint64_t instCount = - ust.v[idx(UploadProfiler::Stat::InstanceUploadCount)]; + upload.v[idx(UploadProfiler::Stat::InstanceUploadCount)]; const std::uint64_t instBytes = - ust.v[idx(UploadProfiler::Stat::InstanceUploadBytes)]; + upload.v[idx(UploadProfiler::Stat::InstanceUploadBytes)]; // lifetime const std::uint64_t stagingCreatedCount = - lt.v[idx(UploadProfiler::Stat::StagingCreatedCount)]; + lifetime.v[idx(UploadProfiler::Stat::StagingCreatedCount)]; const std::uint64_t stagingAllocBytes = - lt.v[idx(UploadProfiler::Stat::StagingAllocatedBytes)]; + lifetime.v[idx(UploadProfiler::Stat::StagingAllocatedBytes)]; const std::uint64_t bufAllocBytes = - lt.v[idx(UploadProfiler::Stat::BufferAllocatedBytes)]; + lifetime.v[idx(UploadProfiler::Stat::BufferAllocatedBytes)]; const std::uint64_t texAllocBytes = - lt.v[idx(UploadProfiler::Stat::TextureAllocatedBytes)]; + lifetime.v[idx(UploadProfiler::Stat::TextureAllocatedBytes)]; const std::uint64_t matAllocBytes = - lt.v[idx(UploadProfiler::Stat::MaterialAllocatedBytes)]; + lifetime.v[idx(UploadProfiler::Stat::MaterialAllocatedBytes)]; const std::uint64_t instAllocBytes = - lt.v[idx(UploadProfiler::Stat::InstanceAllocatedBytes)]; + lifetime.v[idx(UploadProfiler::Stat::InstanceAllocatedBytes)]; std::array memcpyStr{}; std::array stagingUsedStr{}; @@ -237,36 +208,44 @@ static void logUpload(const UploadProfiler &upload) noexcept { std::cerr << line.data() << "\n"; } -void FrameLogger::logPerFrame(const CpuProfiler *cpu, const VkGpuProfiler &gpu, - const UploadProfiler *upload) noexcept { - if (!shouldLog()) { +static void logGpu(const GpuProfiler::Frame &gpu) noexcept { + if (!gpu.valid) { return; } - // TODO: add rolling average over N frames and print that on N frame instead - // of just N frame data - - // NOTE: if queueSubmit is large its likely artifical wait time - // for vsync from FIFO present mode in swapchain + std::array frame{}; + std::array main{}; + std::array idle{}; - // Note: Most of other is likely from the vkWaitForFence in commands on - // submit immediate. I didn't bother logging this since its a pain to - // pass since mesh_store is the one who calls the uploaders who then - // call submit immediate so I would have to pass profiler a lot. But it - // doesn't matter since eventually submit Immediate will be removed and - // we won't have blocking anymore - logCpu(*cpu); + formatMs(frame.data(), frame.size(), gpu.frameMs); + formatMs(main.data(), main.size(), gpu.mainPassMs); + formatMs(idle.data(), idle.size(), gpu.idleGapMs); - logUpload(*upload); - logGpu(gpu); + std::array line{}; + ignore_snprintf(std::snprintf(line.data(), line.size(), + "GPU ms: frame %s main %s idle %s", + frame.data(), main.data(), idle.data())); - std::cout << "\n"; + std::cerr << line.data() << "\n"; } -#ifndef NDEBUG -void emit(Event e, double ms) noexcept { - std::cerr << "[Event] " << name(e) << " ms=" << ms << "\n"; +void logProfilerToConsole(const Telemetry &t) noexcept { + + // TODO: add rolling average over N frames and print that on N frame instead + // of just N frame data + CpuProfiler::Frame cpu{}; + UploadProfiler::Frame upl{}; + UploadProfiler::Frame uplLt{}; + GpuProfiler::Frame gpu{}; + if (!readPublished(t, cpu, upl, uplLt, gpu)) { + return; + } + + std::cerr << "\n[Profiler]\n"; + logCpuFrame(cpu); + logUploadFrame(upl, uplLt); + logGpu(gpu); } -#endif } // namespace profiling +#endif diff --git a/src/backend/profiling/logging/console_sink.hpp b/src/backend/profiling/logging/console_sink.hpp new file mode 100644 index 0000000..7c27e58 --- /dev/null +++ b/src/backend/profiling/logging/console_sink.hpp @@ -0,0 +1,15 @@ +#pragma once + +#if defined(ENABLE_TELEMETRY) +#include "backend/profiling/telemetry/telemetry.hpp" + +namespace profiling { + +inline void ignore_snprintf(int rc) noexcept { (void)rc; } + +#if defined(ENABLE_TELEMETRY) +void logProfilerToConsole(const Telemetry &t) noexcept; +#endif + +} // namespace profiling +#endif diff --git a/src/backend/profiling/logging/profiling_logger.hpp b/src/backend/profiling/logging/profiling_logger.hpp deleted file mode 100644 index 5820537..0000000 --- a/src/backend/profiling/logging/profiling_logger.hpp +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include "backend/profiling/profilers/cpu_profiler.hpp" -#include "backend/profiling/profilers/upload_profiler.hpp" -#include "backend/profiling/profilers/vk_gpu_profiler.hpp" - -#include -#include -#include -#include - -namespace profiling { - -inline void ignore_snprintf(int rc) noexcept { (void)rc; } - -// Per-frame logging -class FrameLogger { -public: - void setPeriod(uint64_t n) noexcept { m_period = n; } - void logPerFrame(const CpuProfiler *cpu, const VkGpuProfiler &gpu, - const UploadProfiler *upload) noexcept; - -private: - bool shouldLog() noexcept; - - uint64_t m_frameCounter = 0; - uint64_t m_period = 120; -}; - -// One-off event logging -enum class Event : std::uint8_t { - DeviceWaitIdle = 0, - SwapchainRecreate, - Count -}; - -static constexpr std::string_view name(Event e) noexcept { - switch (e) { - case Event::DeviceWaitIdle: - return "vkDeviceWaitIdle"; - case Event::SwapchainRecreate: - return "SwapchainRecreate"; - default: - return "Unknown"; - } -} - -#ifndef NDEBUG - -void emit(Event e, double ms) noexcept; - -class EventScope { -public: - EventScope(Event e) noexcept : m_event(e), m_t0(clock::now()) {} - ~EventScope() noexcept { end(); } - - EventScope(const EventScope &) = delete; - EventScope &operator=(const EventScope &) = delete; - EventScope(EventScope &&) = delete; - EventScope &operator=(EventScope &&) = delete; - -private: - using clock = std::chrono::steady_clock; - - void end() noexcept { - const auto t1 = clock::now(); - const double ms = - std::chrono::duration(t1 - m_t0).count(); - emit(m_event, ms); - } - - Event m_event{}; - clock::time_point m_t0; -}; - -#else - -// Release build -inline void emit(Event, double) noexcept {} - -class EventScope { -public: - explicit EventScope(Event) noexcept {} -}; - -#endif - -} // namespace profiling diff --git a/src/backend/profiling/profilers/gpu_frame_stats.hpp b/src/backend/profiling/profilers/gpu_frame_stats.hpp new file mode 100644 index 0000000..47c73c8 --- /dev/null +++ b/src/backend/profiling/profilers/gpu_frame_stats.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace GpuProfiler { + +struct Frame { + bool valid = false; + double frameMs = 0.0; + double mainPassMs = 0.0; + double idleGapMs = 0.0; +}; + +} // namespace GpuProfiler diff --git a/src/backend/profiling/profilers/vk_gpu_profiler.cpp b/src/backend/profiling/profilers/vk_gpu_profiler.cpp index 49d6455..3888392 100644 --- a/src/backend/profiling/profilers/vk_gpu_profiler.cpp +++ b/src/backend/profiling/profilers/vk_gpu_profiler.cpp @@ -1,6 +1,7 @@ #include "backend/profiling/profilers/vk_gpu_profiler.hpp" #include "backend/core/vk_backend_ctx.hpp" +#include "backend/profiling/profilers/gpu_frame_stats.hpp" #include #include @@ -120,9 +121,8 @@ void VkGpuProfiler::markFrameEnd(VkCommandBuffer cmd, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); } -VkGpuProfiler::GpuFrameStats -VkGpuProfiler::tryCollect(uint32_t frameIndex) noexcept { - GpuFrameStats out{}; +GpuProfiler::Frame VkGpuProfiler::tryCollect(uint32_t frameIndex) noexcept { + GpuProfiler::Frame out{}; if (m_pool == VK_NULL_HANDLE || m_framesInFlight == 0) { return out; diff --git a/src/backend/profiling/profilers/vk_gpu_profiler.hpp b/src/backend/profiling/profilers/vk_gpu_profiler.hpp index 8c7b8dd..09dc6f5 100644 --- a/src/backend/profiling/profilers/vk_gpu_profiler.hpp +++ b/src/backend/profiling/profilers/vk_gpu_profiler.hpp @@ -1,6 +1,7 @@ #pragma once #include "backend/core/vk_backend_ctx.hpp" +#include "backend/profiling/profilers/gpu_frame_stats.hpp" #include #include @@ -16,13 +17,6 @@ class VkGpuProfiler { Count }; - struct GpuFrameStats { - bool valid = false; - double frameMs = 0.0; - double mainPassMs = 0.0; - double idleGapMs = 0.0; - }; - VkGpuProfiler() = default; ~VkGpuProfiler() noexcept { shutdown(); } @@ -59,8 +53,10 @@ class VkGpuProfiler { void onFrameSubmitted() noexcept { ++m_submittedFrames; } - [[nodiscard]] GpuFrameStats tryCollect(uint32_t frameIndex) noexcept; - [[nodiscard]] const GpuFrameStats &last() const noexcept { return m_last; } + [[nodiscard]] GpuProfiler::Frame tryCollect(uint32_t frameIndex) noexcept; + [[nodiscard]] const GpuProfiler::Frame &last() const noexcept { + return m_last; + } private: static constexpr uint32_t markersPerFrame() noexcept { @@ -84,5 +80,5 @@ class VkGpuProfiler { std::uint64_t m_lastFrameEndTs = 0; bool m_haveLastFrameEndTs = false; - GpuFrameStats m_last{}; + GpuProfiler::Frame m_last{}; }; diff --git a/src/backend/profiling/telemetry/publish.cpp b/src/backend/profiling/telemetry/publish.cpp index 0187b94..0408377 100644 --- a/src/backend/profiling/telemetry/publish.cpp +++ b/src/backend/profiling/telemetry/publish.cpp @@ -3,6 +3,7 @@ #if defined(ENABLE_TELEMETRY) #include "backend/profiling/profilers/cpu_profiler.hpp" +#include "backend/profiling/profilers/gpu_frame_stats.hpp" #include "backend/profiling/profilers/upload_profiler.hpp" #include "backend/profiling/telemetry/telemetry.hpp" @@ -26,29 +27,67 @@ static inline std::chrono::nanoseconds publishPeriod() noexcept { static inline void publishSeqLock(PublishedTelemetry &p, const CpuProfiler::Frame &cpu, - const UploadProfiler::Frame &upl) noexcept { + const UploadProfiler::Frame &upl, + const UploadProfiler::Frame &uplLifetime, + const GpuProfiler::Frame &gpu) noexcept { p.seq.fetch_add(1, std::memory_order_relaxed); // odd std::atomic_thread_fence(std::memory_order_release); p.cpu = cpu; p.upload = upl; + p.uploadLifetime = uplLifetime; + p.gpu = gpu; std::atomic_thread_fence(std::memory_order_release); p.seq.fetch_add(1, std::memory_order_relaxed); // even } +bool publishMaybe(const GpuProfiler::Frame &gpu) noexcept { + Telemetry *t = telemetry(); + if (t == nullptr) { + return false; + } + + const auto period = publishPeriod(); + if (period.count() <= 0) { + return false; // publishing disabled + } + + const auto now = std::chrono::steady_clock::now(); + + if (t->lastPublish.time_since_epoch().count() == 0) { + publishSeqLock(t->published, t->cpu.last(), t->upload.last(), + t->upload.lifetime(), gpu); + t->lastPublish = now; + return true; + } + + if ((now - t->lastPublish) < period) { + return false; + } + + publishSeqLock(t->published, t->cpu.last(), t->upload.last(), + t->upload.lifetime(), gpu); + t->lastPublish = now; + return true; +} + bool readPublished(const Telemetry &t, CpuProfiler::Frame &outCpu, - UploadProfiler::Frame &outUpload) noexcept { + UploadProfiler::Frame &outUpload, + UploadProfiler::Frame &outUploadLifetime, + GpuProfiler::Frame &outGpu) noexcept { const PublishedTelemetry &p = t.published; for (int tries = 0; tries < 4; ++tries) { const uint32_t a = p.seq.load(std::memory_order_acquire); - if (a & 1U) { + if ((a & 1U) != 0U) { continue; } outCpu = p.cpu; outUpload = p.upload; + outUploadLifetime = p.uploadLifetime; + outGpu = p.gpu; std::atomic_thread_fence(std::memory_order_acquire); const uint32_t b = p.seq.load(std::memory_order_acquire); diff --git a/src/backend/profiling/telemetry/publish.hpp b/src/backend/profiling/telemetry/publish.hpp index 30803e3..bdc6707 100644 --- a/src/backend/profiling/telemetry/publish.hpp +++ b/src/backend/profiling/telemetry/publish.hpp @@ -10,14 +10,15 @@ namespace profiling { -// Time-based publish void setPublishPeriod(std::chrono::nanoseconds period) noexcept; // Opportunistic publish -void publishMaybe() noexcept; +bool publishMaybe(const GpuProfiler::Frame &gpu) noexcept; bool readPublished(const Telemetry &t, CpuProfiler::Frame &outCpu, - UploadProfiler::Frame &outUpload) noexcept; + UploadProfiler::Frame &outUpload, + UploadProfiler::Frame &outUploadLifetime, + GpuProfiler::Frame &outGpu) noexcept; } // namespace profiling #endif diff --git a/src/backend/profiling/telemetry/telemetry.hpp b/src/backend/profiling/telemetry/telemetry.hpp index 1ba23a8..fe6eac2 100644 --- a/src/backend/profiling/telemetry/telemetry.hpp +++ b/src/backend/profiling/telemetry/telemetry.hpp @@ -34,6 +34,7 @@ inline Telemetry *telemetry() noexcept { return tlsTelemetry(); } #if defined(ENABLE_TELEMETRY) #include "backend/profiling/profilers/cpu_profiler.hpp" +#include "backend/profiling/profilers/gpu_frame_stats.hpp" #include "backend/profiling/profilers/upload_profiler.hpp" #include #include @@ -44,6 +45,8 @@ struct alignas(64) PublishedTelemetry { std::atomic seq{0}; // even=stable, odd=writer in progress CpuProfiler::Frame cpu{}; UploadProfiler::Frame upload{}; + UploadProfiler::Frame uploadLifetime{}; + GpuProfiler::Frame gpu{}; }; struct Telemetry { diff --git a/src/backend/profiling/view/profiler_snapshot.cpp b/src/backend/profiling/view/profiler_snapshot.cpp new file mode 100644 index 0000000..e14e7f7 --- /dev/null +++ b/src/backend/profiling/view/profiler_snapshot.cpp @@ -0,0 +1,39 @@ +#include "backend/profiling/view/profiler_snapshot.hpp" + +#if defined(ENABLE_TELEMETRY) +#include "backend/profiling/profilers/cpu_profiler.hpp" +#include "backend/profiling/profilers/gpu_frame_stats.hpp" +#include "backend/profiling/profilers/upload_profiler.hpp" +#include "backend/profiling/telemetry/publish.hpp" +#include "backend/profiling/telemetry/telemetry.hpp" +#endif + +namespace profiling { + +bool buildSnapshotFromTls(ProfilerSnapshot &out) noexcept { + out = {}; + +#if defined(ENABLE_TELEMETRY) + Telemetry *t = telemetry(); + if (t != nullptr) { + CpuProfiler::Frame cpu{}; + UploadProfiler::Frame upl{}; + UploadProfiler::Frame uplLt{}; + GpuProfiler::Frame gpu{}; + if (readPublished(*t, cpu, upl, uplLt, gpu)) { + out.cpu = cpu; + out.uploadFrame = upl; + out.uploadLifetime = t->upload.lifetime(); + out.gpu = gpu; + out.cpuValid = true; + out.uploadValid = true; + out.gpuValid = gpu.valid; + return true; + } + } +#endif + + return false; +} + +} // namespace profiling diff --git a/src/backend/profiling/view/profiler_snapshot.hpp b/src/backend/profiling/view/profiler_snapshot.hpp new file mode 100644 index 0000000..457474c --- /dev/null +++ b/src/backend/profiling/view/profiler_snapshot.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "backend/profiling/profilers/cpu_profiler.hpp" +#include "backend/profiling/profilers/gpu_frame_stats.hpp" +#include "backend/profiling/profilers/upload_profiler.hpp" + +namespace profiling { + +struct ProfilerSnapshot { + bool cpuValid = false; + bool uploadValid = false; + bool gpuValid = false; + + CpuProfiler::Frame cpu{}; + UploadProfiler::Frame uploadFrame{}; + UploadProfiler::Frame uploadLifetime{}; + GpuProfiler::Frame gpu{}; +}; + +bool buildSnapshotFromTls(ProfilerSnapshot &out) noexcept; + +} // namespace profiling diff --git a/src/engine/app/app.cpp b/src/engine/app/app.cpp index 50d3638..33fde5d 100644 --- a/src/engine/app/app.cpp +++ b/src/engine/app/app.cpp @@ -1,6 +1,12 @@ #include "app.hpp" +#if defined(ENABLE_TELEMETRY) +#include "backend/profiling/telemetry/publish.hpp" +#include +#endif + #include "backend/profiling/telemetry/telemetry.hpp" + #include "engine/jobs/job_system.hpp" #include "engine/logging/log.hpp" @@ -13,6 +19,7 @@ bool EngineApp::init(const AppConfig &cfg) { shutdown(); #if defined(ENABLE_TELEMETRY) + profiling::setPublishPeriod(std::chrono::seconds(5)); profiling::setTlsTelemetry(&m_profTelemetry); #else profiling::setTlsTelemetry(nullptr); diff --git a/src/engine/app/app.hpp b/src/engine/app/app.hpp index 0547361..b26fc65 100644 --- a/src/engine/app/app.hpp +++ b/src/engine/app/app.hpp @@ -2,12 +2,16 @@ #include "backend/core/vk_backend_ctx.hpp" #include "backend/presentation/vk_presenter.hpp" -#include "backend/profiling/telemetry/telemetry.hpp" + #include "engine/geometry/mesh_factory.hpp" #include "engine/jobs/job_system.hpp" #include "platform/window/glfw_window.hpp" #include "render/renderer.hpp" +#if defined(ENABLE_TELEMETRY) +#include "backend/profiling/telemetry/telemetry.hpp" +#endif + #include #include #include diff --git a/src/engine/editor/editor_ui.hpp b/src/engine/editor/editor_ui.hpp new file mode 100644 index 0000000..e69de29 diff --git a/src/engine/editor/imgui/imgui_context.hpp b/src/engine/editor/imgui/imgui_context.hpp new file mode 100644 index 0000000..e69de29 diff --git a/src/engine/editor/panels/profiler_panel.hpp b/src/engine/editor/panels/profiler_panel.hpp new file mode 100644 index 0000000..e69de29 diff --git a/src/engine/jobs/job_system.cpp b/src/engine/jobs/job_system.cpp index 266eeb1..ebb6d99 100644 --- a/src/engine/jobs/job_system.cpp +++ b/src/engine/jobs/job_system.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include diff --git a/src/engine/jobs/job_system.hpp b/src/engine/jobs/job_system.hpp index 7c077db..96f0400 100644 --- a/src/engine/jobs/job_system.hpp +++ b/src/engine/jobs/job_system.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index a79c706..20ff235 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -37,6 +37,8 @@ target_link_libraries(quark_render quark::engine::jobs PRIVATE quark::engine::geometry + quark::backend::profiling::logging + quark::backend::profiling::telemetry ) add_library(quark::render ALIAS quark_render) diff --git a/src/render/renderer.cpp b/src/render/renderer.cpp index 637084f..c7de428 100644 --- a/src/render/renderer.cpp +++ b/src/render/renderer.cpp @@ -4,10 +4,15 @@ #include "backend/gpu/upload/vk_upload_context.hpp" #include "backend/presentation/vk_presenter.hpp" -#include "backend/profiling/logging/profiling_logger.hpp" #include "backend/profiling/profilers/vk_gpu_profiler.hpp" + #include "backend/profiling/telemetry/telemetry.hpp" +#if defined(ENABLE_TELEMETRY) +#include "backend/profiling/logging/console_sink.hpp" +#include "backend/profiling/telemetry/publish.hpp" +#endif + #include "engine/geometry/transform.hpp" #include "engine/jobs/job_system.hpp" #include "engine/mesh/mesh_data.hpp" @@ -433,19 +438,13 @@ bool Renderer::drawFrame(VkPresenter &presenter, std::span items) { auto endGuard = makeScopeExit([&] { #if defined(ENABLE_TELEMETRY) - auto *c = profiling::cpuPtr(); - auto *u = profiling::uploadPtr(); - - if (c) { - c->endInterval(); - } + if (auto *t = profiling::telemetry(); t != nullptr) { + t->cpu.endInterval(); + t->upload.endInterval(); - if (u) { - u->endInterval(); - } - - if (c && u) { - m_profileReporter.logPerFrame(c, m_gpuProfiler, u); + if (profiling::publishMaybe(m_gpuProfiler.last())) { + profiling::logProfilerToConsole(*t); + } } #endif }); @@ -534,17 +533,13 @@ bool Renderer::recreateSwapchainDependent(VkPresenter &presenter, const std::string &vertSpvPath, const std::string &fragSpvPath) { LOGW("Recreating swapchain-dependent resources"); - profiling::EventScope scope(profiling::Event::SwapchainRecreate); if (m_ctx == nullptr || m_ctx->device() == VK_NULL_HANDLE) { return false; } VkDevice device = m_ctx->device(); - { - profiling::EventScope w(profiling::Event::DeviceWaitIdle); - vkDeviceWaitIdle(device); - } + vkDeviceWaitIdle(device); if (!presenter.recreateSwapchain()) { LOGE("Swapchain recreation failed"); diff --git a/src/render/renderer.hpp b/src/render/renderer.hpp index 9e982de..083f5a2 100644 --- a/src/render/renderer.hpp +++ b/src/render/renderer.hpp @@ -4,7 +4,6 @@ #include "backend/frame/vk_frame_manager.hpp" #include "backend/presentation/vk_presenter.hpp" -#include "backend/profiling/logging/profiling_logger.hpp" #include "backend/profiling/profilers/vk_gpu_profiler.hpp" #include "render/rendergraph/main_pass.hpp" @@ -85,7 +84,6 @@ class Renderer { shutdown(); m_gpuProfiler = std::move(other.m_gpuProfiler); - m_profileReporter = std::move(other.m_profileReporter); m_framesInFlight = std::exchange(other.m_framesInFlight, 0U); m_ctx = std::exchange(other.m_ctx, nullptr); @@ -170,7 +168,6 @@ class Renderer { std::vector m_swapLayouts; VkGpuProfiler m_gpuProfiler; - profiling::FrameLogger m_profileReporter{}; uint32_t m_framesInFlight = 0; VkBackendCtx *m_ctx = nullptr; // non-owning From dbd94d0a70ca47e508f2018fa29d4ceb05dddc5b Mon Sep 17 00:00:00 2001 From: gituser12981u2 Date: Sun, 1 Feb 2026 14:40:40 -0800 Subject: [PATCH 2/2] feat: add imgui rendering --- CMakeLists.txt | 2 + imgui.ini | 8 + src/backend/CMakeLists.txt | 2 + src/backend/presentation/vk_presenter.hpp | 3 + src/backend/shaders/CMakeLists.txt | 2 - src/backend/ui/CMakeLists.txt | 19 +++ src/backend/ui/imgui/CMakeLists.txt | 18 ++ src/backend/ui/imgui/imgui_overlay_sink.cpp | 180 ++++++++++++++++++++ src/backend/ui/imgui/imgui_overlay_sink.hpp | 38 +++++ src/backend/ui/ui_overlay_sink.hpp | 40 +++++ src/backend/ui/vk_ui_overlay.cpp | 104 +++++++++++ src/backend/ui/vk_ui_overlay.hpp | 65 +++++++ src/engine/CMakeLists.txt | 2 + src/engine/app/CMakeLists.txt | 4 +- src/engine/app/app.cpp | 11 +- src/engine/app/app.hpp | 11 ++ src/engine/editor/CMakeLists.txt | 15 ++ src/engine/editor/editor.cpp | 15 ++ src/engine/editor/editor.hpp | 29 ++++ src/engine/editor/editor_ui.hpp | 0 src/engine/editor/imgui/imgui_context.hpp | 1 + src/render/CMakeLists.txt | 7 +- src/render/renderer.cpp | 32 ++++ src/render/renderer.hpp | 10 ++ src/render/upload/upload_manager.hpp | 2 +- vcpkg.json | 6 +- 26 files changed, 613 insertions(+), 13 deletions(-) create mode 100644 imgui.ini create mode 100644 src/backend/ui/CMakeLists.txt create mode 100644 src/backend/ui/imgui/CMakeLists.txt create mode 100644 src/backend/ui/imgui/imgui_overlay_sink.cpp create mode 100644 src/backend/ui/imgui/imgui_overlay_sink.hpp create mode 100644 src/backend/ui/ui_overlay_sink.hpp create mode 100644 src/backend/ui/vk_ui_overlay.cpp create mode 100644 src/backend/ui/vk_ui_overlay.hpp create mode 100644 src/engine/editor/CMakeLists.txt create mode 100644 src/engine/editor/editor.cpp create mode 100644 src/engine/editor/editor.hpp delete mode 100644 src/engine/editor/editor_ui.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 416bee3..e726fba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,8 @@ find_package(Stb REQUIRED) find_package(spdlog CONFIG REQUIRED) +find_package(imgui CONFIG REQUIRED) + find_path(CGLTF_INCLUDE_DIRS "cgltf.h") add_compile_definitions(ENABLE_TELEMETRY) diff --git a/imgui.ini b/imgui.ini new file mode 100644 index 0000000..a21d8f3 --- /dev/null +++ b/imgui.ini @@ -0,0 +1,8 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 + +[Window][Quark] +Pos=349,116 +Size=236,88 + diff --git a/src/backend/CMakeLists.txt b/src/backend/CMakeLists.txt index 21b4022..74399e1 100644 --- a/src/backend/CMakeLists.txt +++ b/src/backend/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(presentation) add_subdirectory(graphics) add_subdirectory(gpu) add_subdirectory(shaders) +add_subdirectory(ui) add_subdirectory(profiling) add_library(quark_backend INTERFACE) @@ -14,6 +15,7 @@ target_link_libraries(quark_backend INTERFACE quark::backend::graphics quark::backend::gpu quark::backend::shaders + quark::backend::ui quark::backend::profiling ) diff --git a/src/backend/presentation/vk_presenter.hpp b/src/backend/presentation/vk_presenter.hpp index 4f85a20..2d475a9 100644 --- a/src/backend/presentation/vk_presenter.hpp +++ b/src/backend/presentation/vk_presenter.hpp @@ -123,6 +123,9 @@ class VkPresenter { return static_cast(m_swapchain.swapchainImageViews().size()); } + [[nodiscard]] GlfwWindow *window() noexcept { return m_window; } + [[nodiscard]] const GlfwWindow *window() const noexcept { return m_window; } + private: VkBackendCtx *m_ctx = nullptr; // non-owning GlfwWindow *m_window = nullptr; // non-owning diff --git a/src/backend/shaders/CMakeLists.txt b/src/backend/shaders/CMakeLists.txt index 8fa7cc5..a0245dc 100644 --- a/src/backend/shaders/CMakeLists.txt +++ b/src/backend/shaders/CMakeLists.txt @@ -1,5 +1,3 @@ - - add_library(quark_backend_shaders STATIC vk_shader.cpp ) diff --git a/src/backend/ui/CMakeLists.txt b/src/backend/ui/CMakeLists.txt new file mode 100644 index 0000000..db01dc8 --- /dev/null +++ b/src/backend/ui/CMakeLists.txt @@ -0,0 +1,19 @@ +add_subdirectory(imgui) + +add_library(quark_backend_ui STATIC + vk_ui_overlay.cpp +) + +target_include_directories(quark_backend_ui + PUBLIC + ${CMAKE_SOURCE_DIR}/src +) + +target_link_libraries(quark_backend_ui + PRIVATE + quark::backend::core + quark::platform::window + quark::engine::logging +) + +add_library(quark::backend::ui ALIAS quark_backend_ui) diff --git a/src/backend/ui/imgui/CMakeLists.txt b/src/backend/ui/imgui/CMakeLists.txt new file mode 100644 index 0000000..4e6e001 --- /dev/null +++ b/src/backend/ui/imgui/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(quark_backend_ui_imgui STATIC + imgui_overlay_sink.cpp +) + +target_include_directories(quark_backend_ui_imgui + PUBLIC + ${CMAKE_SOURCE_DIR}/src +) + +target_link_libraries(quark_backend_ui_imgui + PRIVATE + quark::backend::core + quark::platform::window + quark::engine::logging + imgui::imgui +) + +add_library(quark::backend::ui::imgui ALIAS quark_backend_ui_imgui) diff --git a/src/backend/ui/imgui/imgui_overlay_sink.cpp b/src/backend/ui/imgui/imgui_overlay_sink.cpp new file mode 100644 index 0000000..0f4e6ac --- /dev/null +++ b/src/backend/ui/imgui/imgui_overlay_sink.cpp @@ -0,0 +1,180 @@ +#include "backend/ui/imgui/imgui_overlay_sink.hpp" + +#include "backend/core/vk_backend_ctx.hpp" +#include "engine/logging/log.hpp" + +#include +#include +#include + +#include +#include + +namespace ui { + +static VkSampleCountFlagBits pickSampleCount1() { + return VK_SAMPLE_COUNT_1_BIT; +} + +bool ImGuiOverlaySink::createDescriptorPool(VkDevice device) noexcept { + const std::array poolSizes{{ + {VK_DESCRIPTOR_TYPE_SAMPLER, 1000}, + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000}, + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000}, + {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000}, + {VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000}, + {VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000}, + {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000}, + }}; + + VkDescriptorPoolCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + info.maxSets = 1000 * static_cast(poolSizes.size()); + info.poolSizeCount = static_cast(poolSizes.size()); + info.pPoolSizes = poolSizes.data(); + + VkResult res = vkCreateDescriptorPool(device, &info, nullptr, &m_descPool); + if (res != VK_SUCCESS) { + LOGE("[ImGuiOverlaySink] vkCreateDescriptorPool failed"); + m_descPool = VK_NULL_HANDLE; + return false; + } + return true; +} + +bool ImGuiOverlaySink::init(VkBackendCtx &ctx, GlfwWindow &window, + uint32_t framesInFlight, VkFormat swapchainFormat) { + shutdown(ctx.device()); + + if (!window.valid()) { + LOGE("[ImGuiOverlaySink] window invalid"); + return false; + } + if (framesInFlight == 0) { + LOGE("[ImGuiOverlaySink] framesInFlight must be > 0"); + return false; + } + if (swapchainFormat == VK_FORMAT_UNDEFINED) { + LOGE("[ImGuiOverlaySink] swapchainFormat invalid"); + return false; + } + + m_framesInFlight = framesInFlight; + m_swapchainFormat = swapchainFormat; + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + + ImGui_ImplGlfw_InitForVulkan(window.handle(), /*install_callbacks=*/true); + + if (!createDescriptorPool(ctx.device())) { + shutdown(ctx.device()); + return false; + } + + VkPipelineRenderingCreateInfo pr{}; + pr.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; + pr.colorAttachmentCount = 1; + pr.pColorAttachmentFormats = &m_swapchainFormat; + pr.depthAttachmentFormat = VK_FORMAT_UNDEFINED; + pr.stencilAttachmentFormat = VK_FORMAT_UNDEFINED; + + ImGui_ImplVulkan_InitInfo initInfo{}; + initInfo.Instance = ctx.instance(); + initInfo.PhysicalDevice = ctx.physicalDevice(); + initInfo.Device = ctx.device(); + initInfo.QueueFamily = ctx.graphicsQueueFamily(); + initInfo.Queue = ctx.graphicsQueue(); + initInfo.PipelineCache = VK_NULL_HANDLE; + initInfo.DescriptorPool = m_descPool; + initInfo.Subpass = 0; + initInfo.MinImageCount = framesInFlight; + initInfo.ImageCount = framesInFlight; + initInfo.MSAASamples = pickSampleCount1(); + initInfo.Allocator = nullptr; + initInfo.CheckVkResultFn = nullptr; + initInfo.UseDynamicRendering = true; + initInfo.PipelineRenderingCreateInfo = pr; + + ImGui_ImplVulkan_Init(&initInfo); + + ImGui_ImplVulkan_CreateFontsTexture(); + + m_initialized = true; + return true; +} + +void ImGuiOverlaySink::destroyDescriptorPool(VkDevice device) noexcept { + if (m_descPool != VK_NULL_HANDLE) { + vkDestroyDescriptorPool(device, m_descPool, nullptr); + m_descPool = VK_NULL_HANDLE; + } +} + +void ImGuiOverlaySink::shutdown(VkDevice device) { + if (!m_initialized) { + return; + } + + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + destroyDescriptorPool(device); + + m_framesInFlight = 0; + m_swapchainFormat = VK_FORMAT_UNDEFINED; + m_initialized = false; +} + +void ImGuiOverlaySink::beginFrame(const BuildFn &buildPanels) { + if (!m_initialized) { + return; + } + + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + if (buildPanels) { + buildPanels(); + } + + ImGui::Render(); +} + +void ImGuiOverlaySink::record(VkCommandBuffer cmd, const OverlayTarget &tgt) { + if (!m_initialized) { + return; + } + if (cmd == VK_NULL_HANDLE || tgt.colorView == VK_NULL_HANDLE) { + return; + } + + // Dynamic rendering overlay: LOAD existing swapchain color + VkRenderingAttachmentInfo colorAttach{}; + colorAttach.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + colorAttach.imageView = tgt.colorView; + colorAttach.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + colorAttach.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + colorAttach.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + + VkRenderingInfo ri{}; + ri.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + ri.renderArea = VkRect2D{.offset = {0, 0}, .extent = tgt.extent}; + ri.layerCount = 1; + ri.colorAttachmentCount = 1; + ri.pColorAttachments = &colorAttach; + + vkCmdBeginRendering(cmd, &ri); + ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd); + vkCmdEndRendering(cmd); +} + +} // namespace ui diff --git a/src/backend/ui/imgui/imgui_overlay_sink.hpp b/src/backend/ui/imgui/imgui_overlay_sink.hpp new file mode 100644 index 0000000..e557b57 --- /dev/null +++ b/src/backend/ui/imgui/imgui_overlay_sink.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "backend/ui/ui_overlay_sink.hpp" + +// TODO get from presenter +#include "platform/window/glfw_window.hpp" + +#include +#include + +class VkBackendCtx; + +namespace ui { + +class ImGuiOverlaySink final : public IOverlaySink { +public: + ImGuiOverlaySink() = default; + ~ImGuiOverlaySink() override { /* explicit shutdown() */ } + + bool init(VkBackendCtx &ctx, GlfwWindow &window, uint32_t framesInFlight, + VkFormat swapchainFormat) override; + + void shutdown(VkDevice device) override; + + void beginFrame(const BuildFn &buildPanels) override; + void record(VkCommandBuffer cmd, const OverlayTarget &tgt) override; + +private: + bool createDescriptorPool(VkDevice device) noexcept; + void destroyDescriptorPool(VkDevice device) noexcept; + + VkDescriptorPool m_descPool = VK_NULL_HANDLE; + uint32_t m_framesInFlight = 0; + VkFormat m_swapchainFormat = VK_FORMAT_UNDEFINED; + bool m_initialized = false; +}; + +} // namespace ui diff --git a/src/backend/ui/ui_overlay_sink.hpp b/src/backend/ui/ui_overlay_sink.hpp new file mode 100644 index 0000000..7be1467 --- /dev/null +++ b/src/backend/ui/ui_overlay_sink.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class VkBackendCtx; +class GlfwWindow; + +namespace ui { + +struct OverlayTarget { + VkImageView colorView = VK_NULL_HANDLE; + VkExtent2D extent{}; + VkFormat colorFormat = VK_FORMAT_UNDEFINED; +}; + +struct IOverlaySink { +public: + using BuildFn = std::function; + + IOverlaySink() = default; + virtual ~IOverlaySink() = default; + + IOverlaySink(const IOverlaySink &) = delete; + IOverlaySink &operator=(const IOverlaySink &) = delete; + IOverlaySink(IOverlaySink &&) = delete; + IOverlaySink &operator=(IOverlaySink &&) = delete; + + virtual bool init(VkBackendCtx &ctx, GlfwWindow &window, + uint32_t framesInFlight, VkFormat swapchainFormat) = 0; + + virtual void shutdown(VkDevice device) = 0; + + virtual void beginFrame(const BuildFn &buildPanels) = 0; + + virtual void record(VkCommandBuffer cmd, const OverlayTarget &tgt) = 0; +}; + +} // namespace ui diff --git a/src/backend/ui/vk_ui_overlay.cpp b/src/backend/ui/vk_ui_overlay.cpp new file mode 100644 index 0000000..52ed081 --- /dev/null +++ b/src/backend/ui/vk_ui_overlay.cpp @@ -0,0 +1,104 @@ +#include "backend/ui/vk_ui_overlay.hpp" + +#include "backend/core/vk_backend_ctx.hpp" +#include "engine/logging/log.hpp" + +// TODO: get from presenter +#include "platform/window/glfw_window.hpp" + +namespace ui { + +bool VkUiOverlay::init(VkBackendCtx &ctx, GlfwWindow &window, + uint32_t framesInFlight, VkFormat swapchainFormat) { + shutdown(); + + if (framesInFlight == 0) { + LOGE("[VkUiOverlay] framesInFlight must be > 0"); + return false; + } + if (swapchainFormat == VK_FORMAT_UNDEFINED) { + LOGE("[VkUiOverlay] swapchainFormat invalid"); + return false; + } + + m_ctx = &ctx; + m_framesInFlight = framesInFlight; + m_swapchainFormat = swapchainFormat; + + if (m_sink == nullptr) { + m_inited = true; + return true; + } + + if (!m_sink->init(ctx, window, framesInFlight, swapchainFormat)) { + LOGE("[VkUiOverlay] sink init failed"); + shutdown(); + return false; + } + + m_inited = true; + return true; +} + +void VkUiOverlay::shutdown() noexcept { + VkDevice device = VK_NULL_HANDLE; + if (m_ctx != nullptr) { + device = m_ctx->device(); + } + + if (m_sink != nullptr && device != VK_NULL_HANDLE) { + m_sink->shutdown(device); + } + + m_ctx = nullptr; + m_framesInFlight = 0; + m_swapchainFormat = VK_FORMAT_UNDEFINED; + m_inited = false; +} + +void VkUiOverlay::beginFrame(const IOverlaySink::BuildFn &buildPanels) { + if (!m_inited || m_sink == nullptr) { + return; + } + m_sink->beginFrame(buildPanels); +} + +void VkUiOverlay::record(VkCommandBuffer cmd, const OverlayTarget &tgt) { + if (!m_inited || m_sink == nullptr) { + return; + } + m_sink->record(cmd, tgt); +} + +bool VkUiOverlay::recreateIfNeeded(GlfwWindow &window, + VkFormat newSwapchainFormat) { + if (!m_inited) { + return false; + } + if (newSwapchainFormat == VK_FORMAT_UNDEFINED) { + return false; + } + if (newSwapchainFormat == m_swapchainFormat) { + return true; + } + + // Re-init sink with new format + if (m_ctx == nullptr) { + return false; + } + + LOGW("[VkUiOverlay] swapchain format changed, reinitializing UI overlay"); + m_swapchainFormat = newSwapchainFormat; + + if (m_sink != nullptr) { + VkDevice device = m_ctx->device(); + if (device != VK_NULL_HANDLE) { + m_sink->shutdown(device); + } + return m_sink->init(*m_ctx, window, m_framesInFlight, m_swapchainFormat); + } + + return true; +} + +} // namespace ui diff --git a/src/backend/ui/vk_ui_overlay.hpp b/src/backend/ui/vk_ui_overlay.hpp new file mode 100644 index 0000000..ea4c2e1 --- /dev/null +++ b/src/backend/ui/vk_ui_overlay.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "backend/ui/ui_overlay_sink.hpp" + +#include +#include + +class VkBackendCtx; +class GlfwWindow; + +namespace ui { + +class VkUiOverlay { +public: + VkUiOverlay() = default; + ~VkUiOverlay() noexcept { shutdown(); } + + VkUiOverlay(const VkUiOverlay &) = delete; + VkUiOverlay &operator=(const VkUiOverlay &) = delete; + + VkUiOverlay(VkUiOverlay &&other) noexcept { *this = std::move(other); } + VkUiOverlay &operator=(VkUiOverlay &&other) noexcept { + if (this == &other) { + return *this; + } + shutdown(); + m_ctx = other.m_ctx; + other.m_ctx = nullptr; + m_sink = other.m_sink; + other.m_sink = nullptr; + m_framesInFlight = other.m_framesInFlight; + other.m_framesInFlight = 0; + m_swapchainFormat = other.m_swapchainFormat; + other.m_swapchainFormat = VK_FORMAT_UNDEFINED; + m_inited = other.m_inited; + other.m_inited = false; + return *this; + } + + // Non-owning sink; lifetime must outlive VkUiOverlay. + void setSink(IOverlaySink *sink) noexcept { m_sink = sink; } + [[nodiscard]] IOverlaySink *sink() noexcept { return m_sink; } + [[nodiscard]] const IOverlaySink *sink() const noexcept { return m_sink; } + + bool init(VkBackendCtx &ctx, GlfwWindow &window, uint32_t framesInFlight, + VkFormat swapchainFormat); + + void shutdown() noexcept; + + void beginFrame(const IOverlaySink::BuildFn &buildPanels); + + void record(VkCommandBuffer cmd, const OverlayTarget &tgt); + + // If swapchain format changes (rare), you can re-init sink here. + bool recreateIfNeeded(GlfwWindow &window, VkFormat newSwapchainFormat); + +private: + VkBackendCtx *m_ctx = nullptr; // non-owning + IOverlaySink *m_sink = nullptr; // non-owning + uint32_t m_framesInFlight = 0; + VkFormat m_swapchainFormat = VK_FORMAT_UNDEFINED; + bool m_inited = false; +}; + +} // namespace ui diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index b0c5436..cb1dd90 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -3,6 +3,7 @@ add_subdirectory(camera) add_subdirectory(geometry) add_subdirectory(assets) add_subdirectory(jobs) +add_subdirectory(editor) add_subdirectory(logging) add_library(quark_engine INTERFACE) @@ -12,6 +13,7 @@ target_link_libraries(quark_engine INTERFACE quark::engine::camera quark::engine::geometry quark::engine::jobs + quark::engine::editor quark::engine::logging ) diff --git a/src/engine/app/CMakeLists.txt b/src/engine/app/CMakeLists.txt index bc7c4ba..62660be 100644 --- a/src/engine/app/CMakeLists.txt +++ b/src/engine/app/CMakeLists.txt @@ -11,13 +11,15 @@ target_link_libraries(quark_engine_app PUBLIC quark::backend::core quark::backend::presentation - quark::render quark::engine::geometry quark::platform::window quark::engine::jobs + PRIVATE Vulkan::Vulkan + quark::engine::editor + quark::backend::ui::imgui ) add_library(quark::engine::app ALIAS quark_engine_app) diff --git a/src/engine/app/app.cpp b/src/engine/app/app.cpp index 33fde5d..b7c9aff 100644 --- a/src/engine/app/app.cpp +++ b/src/engine/app/app.cpp @@ -28,12 +28,10 @@ bool EngineApp::init(const AppConfig &cfg) { m_cfg = cfg; if (!m_window.init(cfg.width, cfg.height, cfg.title)) { - std::cerr << "[App] Failed to init window\n"; return false; } if (!m_jobs.init()) { - std::cerr << "[App] Failed to initialize the job system\n"; return false; } @@ -45,7 +43,6 @@ bool EngineApp::init(const AppConfig &cfg) { } if (!m_ctx.init(platformExtensions, cfg.enableValidation)) { - std::cerr << "[App] VkBackendCtx init failed\n"; shutdown(); return false; } @@ -54,14 +51,14 @@ bool EngineApp::init(const AppConfig &cfg) { uint32_t fbHeight = 0; m_window.framebufferSize(fbWidth, fbHeight); if (!m_presenter.init(m_ctx, &m_window, fbWidth, fbHeight)) { - std::cerr << "[App] Presenter init failed\n"; shutdown(); return false; } + m_renderer.setOverlaySink(&m_imguiOverlay); + m_renderer.setOverlayBuildFn(m_editor.buildFn()); if (!m_renderer.init(m_ctx, m_presenter, cfg.framesInFlight, cfg.vertSpvPath, cfg.fragSpvPath, m_jobs)) { - std::cerr << "[App] Renderer init failed\n"; shutdown(); return false; } @@ -78,6 +75,10 @@ void EngineApp::shutdown() noexcept { } m_renderer.shutdown(); + + m_renderer.setOverlaySink(nullptr); + m_renderer.setOverlayBuildFn({}); + m_presenter.shutdown(); m_ctx.shutdown(); m_jobs.shutdown(); diff --git a/src/engine/app/app.hpp b/src/engine/app/app.hpp index b26fc65..814bba3 100644 --- a/src/engine/app/app.hpp +++ b/src/engine/app/app.hpp @@ -3,6 +3,8 @@ #include "backend/core/vk_backend_ctx.hpp" #include "backend/presentation/vk_presenter.hpp" +#include "backend/ui/imgui/imgui_overlay_sink.hpp" +#include "engine/editor/editor.hpp" #include "engine/geometry/mesh_factory.hpp" #include "engine/jobs/job_system.hpp" #include "platform/window/glfw_window.hpp" @@ -48,6 +50,12 @@ class EngineApp { void run(const std::function &tick); GlfwWindow &window() noexcept { return m_window; } + + editor::Editor &editor() noexcept { return m_editor; } + [[nodiscard]] const editor::Editor &editor() const noexcept { + return m_editor; + } + VkBackendCtx &ctx() noexcept { return m_ctx; } VkPresenter &presenter() noexcept { return m_presenter; } Renderer &renderer() noexcept { return m_renderer; } @@ -68,6 +76,9 @@ class EngineApp { profiling::Telemetry m_profTelemetry; #endif + editor::Editor m_editor; + ui::ImGuiOverlaySink m_imguiOverlay; + AppConfig m_cfg{}; bool m_inited = false; }; diff --git a/src/engine/editor/CMakeLists.txt b/src/engine/editor/CMakeLists.txt new file mode 100644 index 0000000..7b9a20a --- /dev/null +++ b/src/engine/editor/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(quark_engine_editor STATIC + editor.cpp +) + +target_include_directories(quark_engine_editor + PUBLIC + ${CMAKE_SOURCE_DIR}/src +) + +target_link_libraries(quark_engine_editor + PRIVATE + imgui::imgui +) + +add_library(quark::engine::editor ALIAS quark_engine_editor) diff --git a/src/engine/editor/editor.cpp b/src/engine/editor/editor.cpp new file mode 100644 index 0000000..24ea4d8 --- /dev/null +++ b/src/engine/editor/editor.cpp @@ -0,0 +1,15 @@ +#include "engine/editor/editor.hpp" + +#include + +namespace editor { + +void Editor::buildPanels() { + ImGui::Begin("Quark"); + ImGui::Text("Hello World"); + static float f = 0.0F; + ImGui::SliderFloat("float", &f, 0.0F, 1.0F); + ImGui::End(); +} + +} // namespace editor diff --git a/src/engine/editor/editor.hpp b/src/engine/editor/editor.hpp new file mode 100644 index 0000000..6748b37 --- /dev/null +++ b/src/engine/editor/editor.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace editor { + +class Editor { +public: + using BuildFn = std::function; + + Editor() = default; + ~Editor() = default; + + Editor(const Editor &) = delete; + Editor &operator=(const Editor &) = delete; + Editor(Editor &&) = delete; + Editor &operator=(Editor &&) = delete; + + void tick(); + + [[nodiscard]] const BuildFn &buildFn() const noexcept { return m_build; } + +private: + void buildPanels(); + + BuildFn m_build = [this] { buildPanels(); }; +}; + +} // namespace editor diff --git a/src/engine/editor/editor_ui.hpp b/src/engine/editor/editor_ui.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/engine/editor/imgui/imgui_context.hpp b/src/engine/editor/imgui/imgui_context.hpp index e69de29..8b13789 100644 --- a/src/engine/editor/imgui/imgui_context.hpp +++ b/src/engine/editor/imgui/imgui_context.hpp @@ -0,0 +1 @@ + diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 20ff235..40ced98 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -4,12 +4,12 @@ add_subdirectory(scene) add_subdirectory(upload) add_library(quark_render STATIC - renderer.cpp + renderer.cpp ) target_include_directories(quark_render - PUBLIC - ${CMAKE_SOURCE_DIR}/src + PUBLIC + ${CMAKE_SOURCE_DIR}/src ) target_link_libraries(quark_render @@ -39,6 +39,7 @@ target_link_libraries(quark_render quark::engine::geometry quark::backend::profiling::logging quark::backend::profiling::telemetry + quark::backend::ui ) add_library(quark::render ALIAS quark_render) diff --git a/src/render/renderer.cpp b/src/render/renderer.cpp index c7de428..1e66554 100644 --- a/src/render/renderer.cpp +++ b/src/render/renderer.cpp @@ -7,6 +7,7 @@ #include "backend/profiling/profilers/vk_gpu_profiler.hpp" #include "backend/profiling/telemetry/telemetry.hpp" +#include "backend/ui/ui_overlay_sink.hpp" #if defined(ENABLE_TELEMETRY) #include "backend/profiling/logging/console_sink.hpp" @@ -170,6 +171,15 @@ bool Renderer::init(VkBackendCtx &ctx, VkPresenter &presenter, return false; } + if (m_overlaySink != nullptr) { + if (!m_overlaySink->init(*m_ctx, *presenter.window(), m_framesInFlight, + presenter.colorFormat())) { + LOGE("Overlay sink init failed"); + shutdown(); + return false; + } + } + m_swapLayouts.assign(presenter.imageCount(), VK_IMAGE_LAYOUT_UNDEFINED); return true; @@ -186,6 +196,10 @@ void Renderer::shutdown() noexcept { vkDeviceWaitIdle(device); } + if (m_overlaySink != nullptr && m_ctx != nullptr) { + m_overlaySink->shutdown(m_ctx->device()); + } + // Commands-dependents m_frames.shutdown(); m_resources.shutdown(); @@ -332,6 +346,14 @@ void Renderer::recordFrame(VkCommandBuffer cmd, VkPresenter &presenter, vkCmdEndRendering(cmd); m_gpuProfiler.markMainPassEnd(cmd, frameIndex); + if (m_overlaySink != nullptr) { + ui::OverlayTarget tgt{}; + tgt.colorView = scView; + tgt.extent = extent; + tgt.colorFormat = presenter.colorFormat(); + m_overlaySink->record(cmd, tgt); + } + util::cmdImageBarrier( cmd, scImg, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0, @@ -471,6 +493,16 @@ bool Renderer::drawFrame(VkPresenter &presenter, return false; } + // TODO: unite into global beginFrame? + if (m_overlaySink != nullptr) { + const auto &fn = m_overlayBuildFn; + if (fn) { + m_overlaySink->beginFrame(m_overlayBuildFn); + } else { + m_overlaySink->beginFrame([] {}); + } + } + const uint32_t frameIndex = m_frames.currentFrameIndex(); if (!m_uploads.beginFrame(frameIndex)) { diff --git a/src/render/renderer.hpp b/src/render/renderer.hpp index 083f5a2..2b18c24 100644 --- a/src/render/renderer.hpp +++ b/src/render/renderer.hpp @@ -6,6 +6,7 @@ #include "backend/profiling/profilers/vk_gpu_profiler.hpp" +#include "backend/ui/ui_overlay_sink.hpp" #include "render/rendergraph/main_pass.hpp" #include "render/rendergraph/swapchain_targets.hpp" @@ -120,6 +121,12 @@ class Renderer { void setCameraUBO(const CameraUBO &ubo) { m_cameraUbo = ubo; } + void setOverlaySink(ui::IOverlaySink *sink) noexcept { m_overlaySink = sink; } + using OverlayBuildFn = ui::IOverlaySink::BuildFn; + void setOverlayBuildFn(OverlayBuildFn fn) { + m_overlayBuildFn = std::move(fn); + } + // Uploading bool beginUpload(uint32_t frameIndex); bool endUpload(bool wait); @@ -184,6 +191,9 @@ class Renderer { ResourceStore m_resources; + ui::IOverlaySink *m_overlaySink = nullptr; + OverlayBuildFn m_overlayBuildFn; + std::string m_vertPath; std::string m_fragPath; CameraUBO m_cameraUbo{}; diff --git a/src/render/upload/upload_manager.hpp b/src/render/upload/upload_manager.hpp index e3df0c2..53af2dd 100644 --- a/src/render/upload/upload_manager.hpp +++ b/src/render/upload/upload_manager.hpp @@ -60,7 +60,7 @@ class UploadManager { private: VkBackendCtx *m_ctx = nullptr; // non-owning - // + uint32_t m_framesInFlight = 0; uint32_t m_threadCount = 0; diff --git a/vcpkg.json b/vcpkg.json index 3d59a68..10f5243 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,6 +7,10 @@ "stb", "cgltf", "spdlog", - "vulkan-memory-allocator" + "vulkan-memory-allocator", + { + "name": "imgui", + "features": ["glfw-binding", "vulkan-binding"] + } ] }