Unravel Engine C++ Reference
Loading...
Searching...
No Matches
gpu_frame_stats_widgets.cpp
Go to the documentation of this file.
2
4#include <bx/bx.h>
5#include <imgui/imgui.h>
6
7#include <bgfx/bgfx.h>
8
9#include <algorithm>
10#include <array>
11#include <cctype>
12#include <string>
13#include <utility>
14#include <vector>
15
16namespace unravel
17{
18namespace
19{
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};
27
28struct render_pass_entry
29{
30 uint16_t view = 0;
31 uint16_t index = 0;
32 std::string name;
33 std::string display_name;
34 float cpu_ms = 0.0f;
35 float gpu_ms = 0.0f;
36};
37
38struct render_pass_node_item
39{
40 bool is_child = false;
41 size_t index = 0;
42};
43
44struct timer_scale
45{
46 double cpu_to_ms = 0.0;
47 double gpu_to_ms = 0.0;
48};
49
50struct widget_layout
51{
52 float item_height = 0.0f;
54};
55
56struct render_pass_totals
57{
58 float cpu_ms = 0.0f;
59 float gpu_ms = 0.0f;
60};
61
62struct render_pass_node
63{
64 std::string name;
65 std::vector<render_pass_node> children;
66 std::vector<render_pass_entry> entries;
67 std::vector<render_pass_node_item> items;
68 float cpu_ms = 0.0f;
69 float gpu_ms = 0.0f;
70 size_t pass_count = 0;
71};
72
73auto to_lower_copy(const std::string& value) -> std::string
74{
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)); });
78 return result;
79}
80
81auto contains_case_insensitive(const std::string& value, const char* filter) -> bool
82{
83 if(filter == nullptr || filter[0] == '\0')
84 {
85 return true;
86 }
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;
90}
91
92auto trim_copy(const std::string& value) -> std::string
93{
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();
98 if(first >= last)
99 {
100 return {};
101 }
102 return std::string(first, last);
103}
104
105auto split_render_pass_path(const std::string& pass_name, bool group_by_prefix) -> std::vector<std::string>
106{
107 std::vector<std::string> parts;
108 if(!group_by_prefix)
109 {
110 parts.push_back(pass_name);
111 return parts;
112 }
113 size_t start = 0;
114 while(start <= pass_name.size())
115 {
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));
119 if(!part.empty())
120 {
121 parts.push_back(std::move(part));
122 }
123 if(separator_pos == std::string::npos)
124 {
125 break;
126 }
127 start = separator_pos + 1;
128 }
129 if(parts.empty())
130 {
131 parts.push_back(pass_name);
132 }
133 return parts;
134}
135
136auto make_render_pass_entries(const gfx::stats* stats, const timer_scale& scale, bool group_by_prefix)
137 -> std::vector<render_pass_entry>
138{
139 std::vector<render_pass_entry> entries;
140 entries.reserve(stats->numViews);
141 for(uint16_t pos = 0; pos < stats->numViews; ++pos)
142 {
143 const auto& view_stats = stats->viewStats[pos];
144 render_pass_entry entry;
145 entry.view = view_stats.view;
146 entry.index = pos;
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();
150 entry.cpu_ms =
151 static_cast<float>(static_cast<double>(view_stats.cpuTimeEnd - view_stats.cpuTimeBegin) * scale.cpu_to_ms);
152 entry.gpu_ms =
153 static_cast<float>(static_cast<double>(view_stats.gpuTimeEnd - view_stats.gpuTimeBegin) * scale.gpu_to_ms);
154 entries.push_back(std::move(entry));
155 }
156 return entries;
157}
158
159void add_render_pass_entry(render_pass_node& node,
160 const std::vector<std::string>& path,
161 size_t path_index,
162 const render_pass_entry& entry)
163{
164 node.cpu_ms += entry.cpu_ms;
165 node.gpu_ms += entry.gpu_ms;
166 node.pass_count++;
167 if(path_index >= path.size())
168 {
169 node.entries.push_back(entry);
170 node.items.push_back({false, node.entries.size() - 1});
171 return;
172 }
173 const std::string& child_name = path[path_index];
174 if(node.children.empty() || node.children.back().name != child_name)
175 {
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});
180 }
181 add_render_pass_entry(node.children.back(), path, path_index + 1, entry);
182}
183
184auto extract_single_pass_entry(const render_pass_node& node, const std::string& prefix) -> render_pass_entry
185{
186 const std::string display_prefix = prefix.empty() ? node.name : prefix + "/" + node.name;
187 if(!node.entries.empty())
188 {
189 render_pass_entry entry = node.entries.front();
190 entry.display_name = display_prefix + "/" + entry.display_name;
191 return entry;
192 }
193 return extract_single_pass_entry(node.children.front(), display_prefix);
194}
195
196void collapse_single_pass_children(render_pass_node& node)
197{
198 for(render_pass_node& child : node.children)
199 {
200 collapse_single_pass_children(child);
201 }
202 for(render_pass_node_item& item : node.items)
203 {
204 if(!item.is_child)
205 {
206 continue;
207 }
208 const render_pass_node& child = node.children[item.index];
209 if(child.pass_count > 1)
210 {
211 continue;
212 }
213 node.entries.push_back(extract_single_pass_entry(child, {}));
214 item.is_child = false;
215 item.index = node.entries.size() - 1;
216 }
217}
218
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>
221{
222 std::vector<render_pass_node> groups;
223 for(const render_pass_entry& entry : entries)
224 {
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)
230 {
231 continue;
232 }
233 if(groups.empty() || groups.back().name != top_level_name)
234 {
235 render_pass_node group;
236 group.name = top_level_name;
237 groups.push_back(std::move(group));
238 }
239 std::vector<std::string> child_path;
240 if(parts.size() > 2)
241 {
242 child_path.assign(parts.begin() + 1, parts.end() - 1);
243 }
244 add_render_pass_entry(groups.back(), child_path, 0, entry);
245 }
246 for(render_pass_node& group : groups)
247 {
248 collapse_single_pass_children(group);
249 }
250 return groups;
251}
252
253void draw_timing_bar(float value_ms, float max_ms, const ImVec4& color, const char* tooltip)
254{
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())
260 {
261 ImGui::SetItemTooltipEx("%s: %.3f ms", tooltip, value_ms);
262 }
263}
264
265auto percent_of(float value, float total) -> float
266{
267 return total > 0.0f ? (value / total) * 100.0f : 0.0f;
268}
269
270void draw_pass_row(const render_pass_entry& entry, const render_pass_totals& totals, float max_bar_ms)
271{
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");
291 ImGui::PopID();
292}
293
294void draw_render_pass_node(const render_pass_node& node, const render_pass_totals& totals, float max_bar_ms)
295{
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");
313 if(!is_open)
314 {
315 return;
316 }
317 for(const render_pass_node_item& item : node.items)
318 {
319 if(item.is_child)
320 {
321 ImGui::PushID(static_cast<int>(item.index));
322 draw_render_pass_node(node.children[item.index], totals, max_bar_ms);
323 ImGui::PopID();
324 }
325 else
326 {
327 draw_pass_row(node.entries[item.index], totals, max_bar_ms);
328 }
329 }
330 ImGui::TreePop();
331}
332
333void draw_encoder_stats(const gfx::stats* stats, const widget_layout& layout, const timer_scale& scale)
334{
335 if(ImGui::BeginListBox("Encoders##GpuProfiler",
336 ImVec2(ImGui::GetWindowWidth(),
337 static_cast<float>(stats->numEncoders) * layout.item_height_with_spacing)))
338 {
339 ImGuiListClipper clipper;
340 clipper.Begin(stats->numEncoders, layout.item_height);
341
342 while(clipper.Step())
343 {
344 for(int32_t pos = clipper.DisplayStart; pos < clipper.DisplayEnd; ++pos)
345 {
346 const auto& encoder_stats = stats->encoderStats[pos];
347 ImGui::PushID(pos);
348 ImGui::Text("%3d", pos);
349 ImGui::SameLine(64.0f);
350
351 const float max_width = profiler_max_width * profiler_scale;
352 const float cpu_ms =
353 static_cast<float>(static_cast<double>(encoder_stats.cpuTimeEnd - encoder_stats.cpuTimeBegin) *
354 scale.cpu_to_ms);
355 const float cpu_width = bx::clamp(cpu_ms * profiler_scale, 1.0f, max_width);
356
357 if(profiler_statistics_utils::draw_progress_bar(cpu_width, max_width, layout.item_height, cpu_color))
358 {
359 ImGui::SetItemTooltipEx(
360 "Encoder %d\nCPU submit (render thread): %.3f ms",
361 pos,
362 cpu_ms);
363 }
364
365 ImGui::PopID();
366 }
367 }
368 ImGui::EndListBox();
369 }
370}
371
372void draw_view_stats(const gfx::stats* stats, const widget_layout& layout, const timer_scale& scale)
373{
374 (void)layout;
375 static std::array<char, 128> filter = {};
376 static bool group_by_prefix = true;
377 ImGui::AlignTextToFramePadding();
378 ImGui::TextUnformatted("Render Passes");
379 ImGui::SameLine();
380 ImGui::SetNextItemWidth(220.0f);
381 ImGui::InputTextWithHint("##render_pass_filter", "Filter pass or group", filter.data(), filter.size());
382 ImGui::SameLine();
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)
390 {
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;
395 }
396 ImGui::TextDisabled("Visible cost: CPU %8.3f ms | GPU %8.3f ms | %3zu passes in %3zu groups",
397 totals.cpu_ms,
398 totals.gpu_ms,
399 visible_pass_count,
400 groups.size());
401 ImGui::SameLine();
402 ImGui::TextColored(cpu_color, "CPU submit");
403 ImGui::SameLine();
404 ImGui::TextUnformatted("/");
405 ImGui::SameLine();
406 ImGui::TextColored(gpu_color, "GPU execute");
407 if(groups.empty())
408 {
409 ImGui::TextColored(warning_color, "No render passes match the current filter.");
410 return;
411 }
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)))
419 {
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)
430 {
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);
434 ImGui::PopID();
435 }
436 ImGui::EndTable();
437 }
438}
439
440} // namespace
441
442void draw_gpu_submit_profiler_ui(const gfx::stats* stats, bool* enable_profiler)
443{
444 ImGui::AlignTextToFramePadding();
445 ImGui::Text("View/encoder timing:");
446 ImGui::SameLine();
447 if(enable_profiler == nullptr)
448 {
449 return;
450 }
451 if(ImGui::Checkbox("Enable##GpuProfiler", enable_profiler))
452 {
453 gfx::set_debug(*enable_profiler ? BGFX_DEBUG_PROFILER : BGFX_DEBUG_NONE);
454 }
455 if(!*enable_profiler)
456 {
457 ImGui::TextColored(warning_color, "Enable to record per-view CPU submit and GPU execute times.");
458 return;
459 }
460
461 if(!stats)
462 {
463 ImGui::TextColored(warning_color, "No stats.");
464 return;
465 }
466
467 if(stats->numViews == 0)
468 {
469 ImGui::TextColored(warning_color, "No GPU profiling data yet (initializing or no views).");
470 return;
471 }
472
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)};
476
477 draw_encoder_stats(stats, layout, scale);
478
479 ImGui::Separator();
480
481 draw_view_stats(stats, layout, scale);
482}
483
484} // namespace unravel
uint16_t view
size_t pass_count
std::vector< render_pass_node_item > items
uint16_t index
double gpu_to_ms
std::string display_name
double cpu_to_ms
float item_height
std::vector< render_pass_entry > entries
float item_height_with_spacing
std::vector< render_pass_node > children
std::string name
Definition hub.cpp:33
const char * tooltip
bgfx::Stats stats
Definition graphics.h:31
void set_debug(uint32_t _debug)
Definition graphics.cpp:470
void end(encoder *_encoder)
Definition graphics.cpp:427
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