From 41e4cc85d5c6344ba779b0e378967807a4232132 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Tue, 22 Sep 2026 19:25:42 +0800 Subject: [PATCH] feat: RenderGraph GPU timestamp profiling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RG::SetQueryPool 可选注入;Execute 期每 pass 前后各写一枚 writeTimestamp2(BOTTOM_OF_PIPE),索引按帧槽位分区 + 执行序排布 - 每帧录制开头 reset 本槽位查询区间(上一轮结果已在 fence 等待后读取) - Renderer 在 WaitFence 后读 64bit 结果换算毫秒(timestampPeriod 缓存), 经 GetPassNames/GetPassGpuMs 透出给 ImGui 面板显示 - 每槽位首帧跳过读取(查询尚未 primed,避免 VUID-09401) --- App/main.cpp | 9 ++++ .../include/Engine/RenderGraph/RenderGraph.h | 11 +++++ Engine/include/Engine/Renderer/Renderer.h | 13 +++++ Engine/src/RenderGraph/RenderGraph.cpp | 39 +++++++++++++++ Engine/src/Renderer/Renderer.cpp | 48 +++++++++++++++++++ 5 files changed, 120 insertions(+) diff --git a/App/main.cpp b/App/main.cpp index 2c57146..fca69ea 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -154,6 +154,15 @@ protected: renderer.SetBloomStrength(strength); } + // Pass 级 GPU 耗时(RG timestamp) + ImGui::Separator(); + ImGui::Text("GPU Timings"); + const auto& names = renderer.GetPassNames(); + const auto& times = renderer.GetPassGpuMs(); + for (size_t i = 0; i < names.size() && i < times.size(); ++i) { + ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]); + } + ImGui::Separator(); ImGui::Text("fps: %.1f frame: %llu", 1.0f / GetDeltaTime(), diff --git a/Engine/include/Engine/RenderGraph/RenderGraph.h b/Engine/include/Engine/RenderGraph/RenderGraph.h index 8af9577..870b169 100644 --- a/Engine/include/Engine/RenderGraph/RenderGraph.h +++ b/Engine/include/Engine/RenderGraph/RenderGraph.h @@ -227,6 +227,13 @@ namespace Engine::RG { // 每帧执行:按编译好的顺序录命令。要求 cmd 处于 recording 状态。 void Execute(const FrameContext& frame); + // GPU timestamp(可选):注入 pool 后,每个 pass 前后各写一枚 + // 时间戳,索引 = frameSlot * queriesPerFrame + 执行序位置 * 2。 + // pool 由调用方创建(容量 ≥ framesInFlight × queriesPerFrame), + // 读取由调用方在本槽位 fence 等待之后进行。 + void SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame); + std::vector GetPassNames() const; + // ---- 统计 ---- u32 GetPassCount() const; @@ -276,6 +283,10 @@ namespace Engine::RG { // 拓扑排序后的执行序(存 pass 下标) std::vector m_ExecutionOrder; + // GPU timestamp(可选注入) + vk::QueryPool m_QueryPool{}; + u32 m_QueriesPerFrame = 0; + // 物理资源池:key = (format, extent, usage),value = 空闲镜像列表 struct PoolKey { u32 format; diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 1f26ab0..f47531e 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -65,6 +65,7 @@ namespace Engine { void HandleResize(); void UpdateShadowDescriptors(); void UpdateFullscreenDescriptors(); + void ReadGpuTimes(u32 frameSlot); Window& m_Window; @@ -136,6 +137,14 @@ namespace Engine { f32 m_BloomThreshold = 1.0f; // HDR 域亮度阈值 f32 m_BloomStrength = 0.8f; // 叠加强度 + // GPU timestamp(pass 级耗时,ImGui 可读) + vk::QueryPool m_QueryPool; + u32 m_QueriesPerFrame = 0; // 每帧槽位的查询数 = pass 数 × 2 + f32 m_TimestampPeriod = 1.0f; // ns/tick(设备属性缓存) + std::vector m_PassNames; // 执行序 pass 名 + std::vector m_PassGpuMs; // 最近一帧各 pass 耗时 + bool m_TimesPrimed[2] = {false, false}; // 每槽位首帧跳过读取 + public: f32 GetExposure() const { return m_Exposure; } void SetExposure(f32 e) { m_Exposure = e; } @@ -146,6 +155,10 @@ namespace Engine { f32 GetBloomStrength() const { return m_BloomStrength; } void SetBloomStrength(f32 v) { m_BloomStrength = v; } + // pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应) + const std::vector& GetPassNames() const { return m_PassNames; } + const std::vector& GetPassGpuMs() const { return m_PassGpuMs; } + private: vk::Extent2D m_Extent{}; diff --git a/Engine/src/RenderGraph/RenderGraph.cpp b/Engine/src/RenderGraph/RenderGraph.cpp index 304e7ae..fd64755 100644 --- a/Engine/src/RenderGraph/RenderGraph.cpp +++ b/Engine/src/RenderGraph/RenderGraph.cpp @@ -607,14 +607,46 @@ namespace Engine::RG { return entry.physical[frameSlot]->view; } + void RenderGraph::SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame) + { + m_QueryPool = pool; + m_QueriesPerFrame = queriesPerFrame; + } + + std::vector RenderGraph::GetPassNames() const + { + std::vector names; + names.reserve(m_ExecutionOrder.size()); + for (const u32 passIdx : m_ExecutionOrder) { + names.push_back(m_Passes[passIdx].name); + } + return names; + } + // ---------- 执行 ---------- void RenderGraph::Execute(const FrameContext& frame) { + // GPU timestamp:可选。池由 Renderer 创建注入;每帧开头重置本槽位 + // 区间(上一轮结果已在 fence 等待后被读取),每个 pass 前后各一枚 + // 时间戳(BOTTOM_OF_PIPE),索引按执行序排布 + u32 tsPos = 0; + if (m_QueryPool) { + frame.cmd.resetQueryPool(m_QueryPool, + frame.frameSlot * m_QueriesPerFrame, + m_QueriesPerFrame); + } + for (const u32 passIdx : m_ExecutionOrder) { auto& pass = m_Passes[passIdx]; if (pass.culled) continue; + if (m_QueryPool) { + frame.cmd.writeTimestamp2( + vk::PipelineStageFlagBits2::eBottomOfPipe, + m_QueryPool, frame.frameSlot * m_QueriesPerFrame + tsPos * 2); + } + // barrier:按槽位状态挑选变体,批量提交 m_ScratchBarriers.clear(); for (const auto& bp : pass.barriers) { @@ -652,6 +684,13 @@ namespace Engine::RG { ctx.graph = this; pass.exec(ctx); } + + if (m_QueryPool) { + frame.cmd.writeTimestamp2( + vk::PipelineStageFlagBits2::eBottomOfPipe, + m_QueryPool, frame.frameSlot * m_QueriesPerFrame + tsPos * 2 + 1); + } + ++tsPos; } } diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 0cfd086..9b49247 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -258,6 +258,19 @@ namespace Engine UpdateShadowDescriptors(); UpdateFullscreenDescriptors(); + // 12. GPU timestamp:每 pass 两枚(前后),按帧槽位分区 + m_QueriesPerFrame = static_cast(m_RenderGraph->GetPassCount()) * 2; + m_PassNames = m_RenderGraph->GetPassNames(); + m_PassGpuMs.assign(m_RenderGraph->GetPassCount(), 0.0f); + + vk::QueryPoolCreateInfo qpInfo; + qpInfo.setQueryType(vk::QueryType::eTimestamp) + .setQueryCount(m_QueriesPerFrame * kMaxFramesInFlight); + m_QueryPool = m_Device->GetDevice().createQueryPool(qpInfo); + m_RenderGraph->SetQueryPool(m_QueryPool, m_QueriesPerFrame); + m_TimestampPeriod = static_cast( + m_Device->GetPhysicalDevice().getProperties().limits.timestampPeriod); + ENGINE_INFO("Renderer initialized"); } @@ -294,6 +307,9 @@ namespace Engine vkDevice.destroySampler(m_ShadowSampler); vkDevice.destroySampler(m_TonemapSampler); + if (m_QueryPool) { + vkDevice.destroyQueryPool(m_QueryPool); + } vkDevice.destroyPipelineLayout(m_PipelineLayout); vkDevice.destroyPipelineLayout(m_ShadowPipelineLayout); vkDevice.destroyPipelineLayout(m_TonemapPipelineLayout); @@ -804,6 +820,37 @@ namespace Engine } } + void Renderer::ReadGpuTimes(u32 frameSlot) + { + if (!m_QueryPool || m_QueriesPerFrame == 0) return; + + // 每槽位首帧:该区间的查询尚未被 reset/写入,跳过一次读取 + if (!m_TimesPrimed[frameSlot]) { + m_TimesPrimed[frameSlot] = true; + return; + } + + const u32 passCount = m_QueriesPerFrame / 2; + std::vector ticks(m_QueriesPerFrame, 0); + + // fence 已等待本槽位上一轮完成,结果必然可用(阻塞式 + 64bit) + const vk::Result r = m_Device->GetDevice().getQueryPoolResults( + m_QueryPool, + frameSlot * m_QueriesPerFrame, m_QueriesPerFrame, + sizeof(u64) * m_QueriesPerFrame, + ticks.data(), sizeof(u64), + vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait); + if (r != vk::Result::eSuccess) return; + + for (u32 i = 0; i < passCount; ++i) { + const u64 start = ticks[i * 2]; + const u64 end = ticks[i * 2 + 1]; + m_PassGpuMs[i] = (end >= start) + ? static_cast(end - start) * m_TimestampPeriod / 1.0e6f + : 0.0f; + } + } + void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets) { m_BoundScene = &scene; @@ -880,6 +927,7 @@ namespace Engine // 此时本槽位的 fence 尚未 reset——先等它(保证 GPU 已完成本槽位上一轮), // 再写 UBO,写-读竞争关闭。 m_SyncManager->WaitFence(frameSlot); + ReadGpuTimes(frameSlot); // fence 已等:上一轮该槽位的查询已就绪 m_UniformBuffer->Update(frameSlot, &ubo, sizeof(ubo)); m_SyncManager->ResetFence(frameSlot);