feat: RenderGraph GPU timestamp profiling

- RG::SetQueryPool 可选注入;Execute 期每 pass 前后各写一枚
  writeTimestamp2(BOTTOM_OF_PIPE),索引按帧槽位分区 + 执行序排布
- 每帧录制开头 reset 本槽位查询区间(上一轮结果已在 fence 等待后读取)
- Renderer 在 WaitFence 后读 64bit 结果换算毫秒(timestampPeriod 缓存),
  经 GetPassNames/GetPassGpuMs 透出给 ImGui 面板显示
- 每槽位首帧跳过读取(查询尚未 primed,避免 VUID-09401)
This commit is contained in:
2026-09-22 19:25:42 +08:00
parent 908e688178
commit 41e4cc85d5
5 changed files with 120 additions and 0 deletions
+9
View File
@@ -154,6 +154,15 @@ protected:
renderer.SetBloomStrength(strength); 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::Separator();
ImGui::Text("fps: %.1f frame: %llu", ImGui::Text("fps: %.1f frame: %llu",
1.0f / GetDeltaTime(), 1.0f / GetDeltaTime(),
@@ -227,6 +227,13 @@ namespace Engine::RG {
// 每帧执行:按编译好的顺序录命令。要求 cmd 处于 recording 状态。 // 每帧执行:按编译好的顺序录命令。要求 cmd 处于 recording 状态。
void Execute(const FrameContext& frame); 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<std::string> GetPassNames() const;
// ---- 统计 ---- // ---- 统计 ----
u32 GetPassCount() const; u32 GetPassCount() const;
@@ -276,6 +283,10 @@ namespace Engine::RG {
// 拓扑排序后的执行序(存 pass 下标) // 拓扑排序后的执行序(存 pass 下标)
std::vector<u32> m_ExecutionOrder; std::vector<u32> m_ExecutionOrder;
// GPU timestamp(可选注入)
vk::QueryPool m_QueryPool{};
u32 m_QueriesPerFrame = 0;
// 物理资源池:key = (format, extent, usage),value = 空闲镜像列表 // 物理资源池:key = (format, extent, usage),value = 空闲镜像列表
struct PoolKey { struct PoolKey {
u32 format; u32 format;
+13
View File
@@ -65,6 +65,7 @@ namespace Engine {
void HandleResize(); void HandleResize();
void UpdateShadowDescriptors(); void UpdateShadowDescriptors();
void UpdateFullscreenDescriptors(); void UpdateFullscreenDescriptors();
void ReadGpuTimes(u32 frameSlot);
Window& m_Window; Window& m_Window;
@@ -136,6 +137,14 @@ namespace Engine {
f32 m_BloomThreshold = 1.0f; // HDR 域亮度阈值 f32 m_BloomThreshold = 1.0f; // HDR 域亮度阈值
f32 m_BloomStrength = 0.8f; // 叠加强度 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<std::string> m_PassNames; // 执行序 pass 名
std::vector<f32> m_PassGpuMs; // 最近一帧各 pass 耗时
bool m_TimesPrimed[2] = {false, false}; // 每槽位首帧跳过读取
public: public:
f32 GetExposure() const { return m_Exposure; } f32 GetExposure() const { return m_Exposure; }
void SetExposure(f32 e) { m_Exposure = e; } void SetExposure(f32 e) { m_Exposure = e; }
@@ -146,6 +155,10 @@ namespace Engine {
f32 GetBloomStrength() const { return m_BloomStrength; } f32 GetBloomStrength() const { return m_BloomStrength; }
void SetBloomStrength(f32 v) { m_BloomStrength = v; } void SetBloomStrength(f32 v) { m_BloomStrength = v; }
// pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应)
const std::vector<std::string>& GetPassNames() const { return m_PassNames; }
const std::vector<f32>& GetPassGpuMs() const { return m_PassGpuMs; }
private: private:
vk::Extent2D m_Extent{}; vk::Extent2D m_Extent{};
+39
View File
@@ -607,14 +607,46 @@ namespace Engine::RG {
return entry.physical[frameSlot]->view; return entry.physical[frameSlot]->view;
} }
void RenderGraph::SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame)
{
m_QueryPool = pool;
m_QueriesPerFrame = queriesPerFrame;
}
std::vector<std::string> RenderGraph::GetPassNames() const
{
std::vector<std::string> 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) 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) { for (const u32 passIdx : m_ExecutionOrder) {
auto& pass = m_Passes[passIdx]; auto& pass = m_Passes[passIdx];
if (pass.culled) continue; if (pass.culled) continue;
if (m_QueryPool) {
frame.cmd.writeTimestamp2(
vk::PipelineStageFlagBits2::eBottomOfPipe,
m_QueryPool, frame.frameSlot * m_QueriesPerFrame + tsPos * 2);
}
// barrier:按槽位状态挑选变体,批量提交 // barrier:按槽位状态挑选变体,批量提交
m_ScratchBarriers.clear(); m_ScratchBarriers.clear();
for (const auto& bp : pass.barriers) { for (const auto& bp : pass.barriers) {
@@ -652,6 +684,13 @@ namespace Engine::RG {
ctx.graph = this; ctx.graph = this;
pass.exec(ctx); pass.exec(ctx);
} }
if (m_QueryPool) {
frame.cmd.writeTimestamp2(
vk::PipelineStageFlagBits2::eBottomOfPipe,
m_QueryPool, frame.frameSlot * m_QueriesPerFrame + tsPos * 2 + 1);
}
++tsPos;
} }
} }
+48
View File
@@ -258,6 +258,19 @@ namespace Engine
UpdateShadowDescriptors(); UpdateShadowDescriptors();
UpdateFullscreenDescriptors(); UpdateFullscreenDescriptors();
// 12. GPU timestamp:每 pass 两枚(前后),按帧槽位分区
m_QueriesPerFrame = static_cast<u32>(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<f32>(
m_Device->GetPhysicalDevice().getProperties().limits.timestampPeriod);
ENGINE_INFO("Renderer initialized"); ENGINE_INFO("Renderer initialized");
} }
@@ -294,6 +307,9 @@ namespace Engine
vkDevice.destroySampler(m_ShadowSampler); vkDevice.destroySampler(m_ShadowSampler);
vkDevice.destroySampler(m_TonemapSampler); vkDevice.destroySampler(m_TonemapSampler);
if (m_QueryPool) {
vkDevice.destroyQueryPool(m_QueryPool);
}
vkDevice.destroyPipelineLayout(m_PipelineLayout); vkDevice.destroyPipelineLayout(m_PipelineLayout);
vkDevice.destroyPipelineLayout(m_ShadowPipelineLayout); vkDevice.destroyPipelineLayout(m_ShadowPipelineLayout);
vkDevice.destroyPipelineLayout(m_TonemapPipelineLayout); 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<u64> 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<f32>(end - start) * m_TimestampPeriod / 1.0e6f
: 0.0f;
}
}
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets) void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets)
{ {
m_BoundScene = &scene; m_BoundScene = &scene;
@@ -880,6 +927,7 @@ namespace Engine
// 此时本槽位的 fence 尚未 reset——先等它(保证 GPU 已完成本槽位上一轮), // 此时本槽位的 fence 尚未 reset——先等它(保证 GPU 已完成本槽位上一轮),
// 再写 UBO,写-读竞争关闭。 // 再写 UBO,写-读竞争关闭。
m_SyncManager->WaitFence(frameSlot); m_SyncManager->WaitFence(frameSlot);
ReadGpuTimes(frameSlot); // fence 已等:上一轮该槽位的查询已就绪
m_UniformBuffer->Update(frameSlot, &ubo, sizeof(ubo)); m_UniformBuffer->Update(frameSlot, &ubo, sizeof(ubo));
m_SyncManager->ResetFence(frameSlot); m_SyncManager->ResetFence(frameSlot);