5#include <imgui/imgui.h>
20constexpr float profiler_scale = 3.0f;
21constexpr float profiler_max_width = 30.0f;
22constexpr float render_pass_table_max_height = 520.0f;
23constexpr float render_pass_table_min_height = 180.0f;
24constexpr ImVec4 cpu_color{0.2f, 0.8f, 0.2f, 1.0f};
25constexpr ImVec4 gpu_color{0.2f, 0.6f, 1.0f, 1.0f};
26constexpr ImVec4 warning_color{1.0f, 0.7f, 0.0f, 1.0f};
28struct render_pass_entry
38struct render_pass_node_item
56struct render_pass_totals
62struct render_pass_node
67 std::vector<render_pass_node_item>
items;
73auto to_lower_copy(
const std::string& value) -> std::string
75 std::string result = value;
76 std::transform(result.begin(), result.end(), result.begin(),
77 [](
unsigned char c) ->
char { return static_cast<char>(std::tolower(c)); });
81auto contains_case_insensitive(
const std::string& value,
const char* filter) ->
bool
83 if(filter ==
nullptr || filter[0] ==
'\0')
87 const std::string lower_value = to_lower_copy(value);
88 const std::string lower_filter = to_lower_copy(filter);
89 return lower_value.find(lower_filter) != std::string::npos;
92auto trim_copy(
const std::string& value) -> std::string
94 const auto first = std::find_if(value.begin(), value.end(),
95 [](
unsigned char c) ->
bool { return std::isspace(c) == 0; });
96 const auto last = std::find_if(value.rbegin(), value.rend(),
97 [](
unsigned char c) ->
bool { return std::isspace(c) == 0; }).base();
102 return std::string(first, last);
105auto split_render_pass_path(
const std::string& pass_name,
bool group_by_prefix) -> std::vector<std::string>
107 std::vector<std::string> parts;
110 parts.push_back(pass_name);
114 while(
start <= pass_name.size())
116 const size_t separator_pos = pass_name.find(
'/',
start);
117 const size_t end = separator_pos == std::string::npos ? pass_name.size() : separator_pos;
118 std::string part = trim_copy(pass_name.substr(
start, end -
start));
121 parts.push_back(std::move(part));
123 if(separator_pos == std::string::npos)
127 start = separator_pos + 1;
131 parts.push_back(pass_name);
136auto make_render_pass_entries(
const gfx::stats* stats,
const timer_scale&
scale,
bool group_by_prefix)
137 -> std::vector<render_pass_entry>
139 std::vector<render_pass_entry>
entries;
141 for(uint16_t pos = 0; pos <
stats->numViews; ++pos)
143 const auto& view_stats =
stats->viewStats[pos];
144 render_pass_entry
entry;
145 entry.view = view_stats.view;
147 entry.name = view_stats.name;
148 const std::vector<std::string> parts = split_render_pass_path(
entry.name, group_by_prefix);
149 entry.display_name = parts.empty() ?
entry.name : parts.back();
151 static_cast<float>(
static_cast<double>(view_stats.cpuTimeEnd - view_stats.cpuTimeBegin) *
scale.cpu_to_ms);
153 static_cast<float>(
static_cast<double>(view_stats.gpuTimeEnd - view_stats.gpuTimeBegin) *
scale.gpu_to_ms);
159void add_render_pass_entry(render_pass_node& node,
160 const std::vector<std::string>& path,
162 const render_pass_entry&
entry)
164 node.cpu_ms +=
entry.cpu_ms;
165 node.gpu_ms +=
entry.gpu_ms;
167 if(path_index >= path.size())
169 node.entries.push_back(
entry);
170 node.items.push_back({
false, node.entries.size() - 1});
173 const std::string& child_name = path[path_index];
174 if(node.children.empty() || node.children.back().name != child_name)
176 render_pass_node child;
177 child.name = child_name;
178 node.children.push_back(std::move(child));
179 node.items.push_back({
true, node.children.size() - 1});
181 add_render_pass_entry(node.children.back(), path, path_index + 1,
entry);
184auto extract_single_pass_entry(
const render_pass_node& node,
const std::string& prefix) -> render_pass_entry
186 const std::string display_prefix = prefix.empty() ? node.name : prefix +
"/" + node.name;
187 if(!node.entries.empty())
189 render_pass_entry
entry = node.entries.front();
190 entry.display_name = display_prefix +
"/" +
entry.display_name;
193 return extract_single_pass_entry(node.children.front(), display_prefix);
196void collapse_single_pass_children(render_pass_node& node)
198 for(render_pass_node& child : node.children)
200 collapse_single_pass_children(child);
202 for(render_pass_node_item& item : node.items)
208 const render_pass_node& child = node.children[item.index];
209 if(child.pass_count > 1)
213 node.entries.push_back(extract_single_pass_entry(child, {}));
214 item.is_child =
false;
215 item.index = node.entries.size() - 1;
219auto make_filtered_render_pass_groups(
const std::vector<render_pass_entry>&
entries,
const char* filter,
bool group_by_prefix)
220 -> std::vector<render_pass_node>
222 std::vector<render_pass_node> groups;
225 const std::vector<std::string> parts = split_render_pass_path(
entry.name, group_by_prefix);
226 const std::string& top_level_name = parts.front();
227 const bool group_matches = contains_case_insensitive(top_level_name, filter);
228 const bool pass_matches = contains_case_insensitive(
entry.name, filter);
229 if(!group_matches && !pass_matches)
233 if(groups.empty() || groups.back().name != top_level_name)
235 render_pass_node group;
236 group.name = top_level_name;
237 groups.push_back(std::move(group));
239 std::vector<std::string> child_path;
242 child_path.assign(parts.begin() + 1, parts.end() - 1);
244 add_render_pass_entry(groups.back(), child_path, 0,
entry);
246 for(render_pass_node& group : groups)
248 collapse_single_pass_children(group);
253void draw_timing_bar(
float value_ms,
float max_ms,
const ImVec4&
color,
const char*
tooltip)
255 const float bar_fraction = max_ms > 0.0f ? std::clamp(value_ms / max_ms, 0.0f, 1.0f) : 0.0f;
256 ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
color);
257 ImGui::ProgressBar(bar_fraction, ImVec2(-1.0f, ImGui::GetFrameHeight() * 0.72f),
"");
258 ImGui::PopStyleColor();
259 if(ImGui::IsItemHovered())
261 ImGui::SetItemTooltipEx(
"%s: %.3f ms",
tooltip, value_ms);
265auto percent_of(
float value,
float total) ->
float
267 return total > 0.0f ? (value / total) * 100.0f : 0.0f;
270void draw_pass_row(
const render_pass_entry&
entry,
const render_pass_totals& totals,
float max_bar_ms)
272 ImGui::PushID(
entry.index);
273 ImGui::TableNextRow();
274 ImGui::TableNextColumn();
275 ImGui::TreeNodeEx(
"pass", ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen |
276 ImGuiTreeNodeFlags_Bullet | ImGuiTreeNodeFlags_SpanFullWidth,
277 "%3u. %s",
entry.view,
entry.display_name.c_str());
278 ImGui::SetItemTooltipEx(
"View %u\n%s",
entry.view,
entry.name.c_str());
279 ImGui::TableNextColumn();
280 ImGui::Text(
"%.3f ms",
entry.cpu_ms);
281 ImGui::TableNextColumn();
282 ImGui::Text(
"%.1f%%", percent_of(
entry.cpu_ms, totals.cpu_ms));
283 ImGui::TableNextColumn();
284 draw_timing_bar(
entry.cpu_ms, max_bar_ms, cpu_color,
"CPU submit");
285 ImGui::TableNextColumn();
286 ImGui::Text(
"%.3f ms",
entry.gpu_ms);
287 ImGui::TableNextColumn();
288 ImGui::Text(
"%.1f%%", percent_of(
entry.gpu_ms, totals.gpu_ms));
289 ImGui::TableNextColumn();
290 draw_timing_bar(
entry.gpu_ms, max_bar_ms, gpu_color,
"GPU execute");
294void draw_render_pass_node(
const render_pass_node& node,
const render_pass_totals& totals,
float max_bar_ms)
296 const ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_DefaultOpen |
297 ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
298 ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
299 ImGui::TableNextColumn();
300 const bool is_open = ImGui::TreeNodeEx(
"node", flags,
"%s (%zu passes)", node.name.c_str(), node.pass_count);
301 ImGui::TableNextColumn();
302 ImGui::Text(
"%.3f ms", node.cpu_ms);
303 ImGui::TableNextColumn();
304 ImGui::Text(
"%.1f%%", percent_of(node.cpu_ms, totals.cpu_ms));
305 ImGui::TableNextColumn();
306 draw_timing_bar(node.cpu_ms, max_bar_ms, cpu_color,
"Group CPU submit");
307 ImGui::TableNextColumn();
308 ImGui::Text(
"%.3f ms", node.gpu_ms);
309 ImGui::TableNextColumn();
310 ImGui::Text(
"%.1f%%", percent_of(node.gpu_ms, totals.gpu_ms));
311 ImGui::TableNextColumn();
312 draw_timing_bar(node.gpu_ms, max_bar_ms, gpu_color,
"Group GPU execute");
317 for(
const render_pass_node_item& item : node.items)
321 ImGui::PushID(
static_cast<int>(item.index));
322 draw_render_pass_node(node.children[item.index], totals, max_bar_ms);
327 draw_pass_row(node.entries[item.index], totals, max_bar_ms);
333void draw_encoder_stats(
const gfx::stats* stats,
const widget_layout& layout,
const timer_scale&
scale)
335 if(ImGui::BeginListBox(
"Encoders##GpuProfiler",
336 ImVec2(ImGui::GetWindowWidth(),
337 static_cast<float>(
stats->numEncoders) * layout.item_height_with_spacing)))
339 ImGuiListClipper clipper;
340 clipper.Begin(
stats->numEncoders, layout.item_height);
342 while(clipper.Step())
344 for(int32_t pos = clipper.DisplayStart; pos < clipper.DisplayEnd; ++pos)
346 const auto& encoder_stats =
stats->encoderStats[pos];
348 ImGui::Text(
"%3d", pos);
349 ImGui::SameLine(64.0f);
351 const float max_width = profiler_max_width * profiler_scale;
353 static_cast<float>(
static_cast<double>(encoder_stats.cpuTimeEnd - encoder_stats.cpuTimeBegin) *
355 const float cpu_width = bx::clamp(
cpu_ms * profiler_scale, 1.0f, max_width);
359 ImGui::SetItemTooltipEx(
360 "Encoder %d\nCPU submit (render thread): %.3f ms",
372void draw_view_stats(
const gfx::stats* stats,
const widget_layout& layout,
const timer_scale&
scale)
375 static std::array<char, 128> filter = {};
376 static bool group_by_prefix =
true;
377 ImGui::AlignTextToFramePadding();
378 ImGui::TextUnformatted(
"Render Passes");
380 ImGui::SetNextItemWidth(220.0f);
381 ImGui::InputTextWithHint(
"##render_pass_filter",
"Filter pass or group", filter.data(), filter.size());
383 ImGui::Checkbox(
"Group by prefix", &group_by_prefix);
384 const std::vector<render_pass_entry>
entries = make_render_pass_entries(stats,
scale, group_by_prefix);
385 std::vector<render_pass_node> groups = make_filtered_render_pass_groups(
entries, filter.data(), group_by_prefix);
386 render_pass_totals totals;
387 float max_bar_ms = 0.0001f;
388 size_t visible_pass_count = 0;
389 for(
const render_pass_node& group : groups)
391 totals.cpu_ms += group.cpu_ms;
392 totals.gpu_ms += group.gpu_ms;
393 max_bar_ms = std::max(max_bar_ms, std::max(group.cpu_ms, group.gpu_ms));
394 visible_pass_count += group.pass_count;
396 ImGui::TextDisabled(
"Visible cost: CPU %8.3f ms | GPU %8.3f ms | %3zu passes in %3zu groups",
402 ImGui::TextColored(cpu_color,
"CPU submit");
404 ImGui::TextUnformatted(
"/");
406 ImGui::TextColored(gpu_color,
"GPU execute");
409 ImGui::TextColored(warning_color,
"No render passes match the current filter.");
412 const float avail_h = ImGui::GetContentRegionAvail().y;
413 const float table_h = std::max(render_pass_table_min_height,
414 std::min(render_pass_table_max_height, std::max(240.0f, avail_h)));
415 constexpr ImGuiTableFlags table_flags = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH |
416 ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY |
417 ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable;
418 if(ImGui::BeginTable(
"##render_pass_groups", 7, table_flags, ImVec2(-1.0f, table_h)))
420 ImGui::TableSetupScrollFreeze(0, 1);
421 ImGui::TableSetupColumn(
"Pass / Group", ImGuiTableColumnFlags_WidthStretch);
422 ImGui::TableSetupColumn(
"CPU Time", ImGuiTableColumnFlags_WidthFixed, 76.0f);
423 ImGui::TableSetupColumn(
"CPU %", ImGuiTableColumnFlags_WidthFixed, 58.0f);
424 ImGui::TableSetupColumn(
"CPU Bar", ImGuiTableColumnFlags_WidthFixed, 150.0f);
425 ImGui::TableSetupColumn(
"GPU Time", ImGuiTableColumnFlags_WidthFixed, 76.0f);
426 ImGui::TableSetupColumn(
"GPU %", ImGuiTableColumnFlags_WidthFixed, 58.0f);
427 ImGui::TableSetupColumn(
"GPU Bar", ImGuiTableColumnFlags_WidthFixed, 190.0f);
428 ImGui::TableHeadersRow();
429 for(
size_t group_index = 0; group_index < groups.size(); ++group_index)
431 const render_pass_node& group = groups[group_index];
432 ImGui::PushID(
static_cast<int>(group_index));
433 draw_render_pass_node(group, totals, max_bar_ms);
444 ImGui::AlignTextToFramePadding();
445 ImGui::Text(
"View/encoder timing:");
447 if(enable_profiler ==
nullptr)
451 if(ImGui::Checkbox(
"Enable##GpuProfiler", enable_profiler))
453 gfx::set_debug(*enable_profiler ? BGFX_DEBUG_PROFILER : BGFX_DEBUG_NONE);
455 if(!*enable_profiler)
457 ImGui::TextColored(warning_color,
"Enable to record per-view CPU submit and GPU execute times.");
463 ImGui::TextColored(warning_color,
"No stats.");
467 if(stats->numViews == 0)
469 ImGui::TextColored(warning_color,
"No GPU profiling data yet (initializing or no views).");
473 const widget_layout layout{ImGui::GetTextLineHeightWithSpacing(), ImGui::GetFrameHeightWithSpacing()};
474 const timer_scale
scale{1000.0 /
static_cast<double>(stats->cpuTimerFreq),
475 1000.0 /
static_cast<double>(stats->gpuTimerFreq)};
477 draw_encoder_stats(stats, layout,
scale);
481 draw_view_stats(stats, layout,
scale);
void set_debug(uint32_t _debug)
void end(encoder *_encoder)
auto draw_progress_bar(float width, float max_width, float height, const ImVec4 &color) -> bool
void draw_gpu_submit_profiler_ui(const gfx::stats *stats, bool *enable_profiler)
Checkbox to toggle BGFX_DEBUG_PROFILER, then encoder (CPU submit) and view (CPU submit / GPU execute)...
std::vector< math::color > color
std::vector< float > scale
std::vector< math::vec3 > start