tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
panels.h
1#pragma once
2
3// Reference Dear ImGui debug panels over the diagnostics export snapshots.
4//
5// This header is opt-in and dependency-free for tess core: it is compiled only
6// when the CONSUMER defines both TESS_ENABLE_IMGUI and TESS_ENABLE_DIAGNOSTICS
7// on its own target, and it never fetches or links Dear ImGui itself. The
8// consumer must include <imgui.h> BEFORE this header (the panels call into the
9// ImGui namespace); when the gates are on but ImGui has not been included, the
10// #error below fires instead of emitting confusing name-lookup failures. The
11// timing panel requires the Dear ImGui tables API available since 1.80.
12//
13// tess.h does NOT include this header, so a diagnostics build that does not use
14// ImGui never sees it. The panels use supported ImGui text and table APIs, and
15// uint64 values are printed through unsigned long long casts for portable
16// printf-style formatting.
17//
18// Threading: the panels only read the snapshot copies they are passed, so they
19// are safe on a render thread -- but producing those copies is not. Capture
20// them per export.h's threading contract (on the recording thread, or with
21// capture externally synchronized against recording), then hand the snapshot
22// to the thread that draws.
23
24#include <tess/diagnostics/diagnostics.h>
25
26#if defined(TESS_ENABLE_IMGUI) && TESS_DIAGNOSTICS_ENABLED
27
28#ifndef IMGUI_VERSION
29#error "tess/debug/imgui/panels.h requires <imgui.h> to be included first"
30#endif
31
32#ifndef IMGUI_HAS_TABLE
33#error "tess/debug/imgui/panels.h requires Dear ImGui 1.80 or later"
34#endif
35
36#include <tess/diagnostics/export.h>
37#include <tess/diagnostics/trace.h>
38
39#include <array>
40#include <charconv>
41#include <cstddef>
42#include <cstdint>
43#include <limits>
44
45namespace tess::debug::imgui {
46
47namespace detail {
48[[nodiscard]] inline auto to_ull(std::uint64_t value) noexcept
49 -> unsigned long long {
50 return static_cast<unsigned long long>(value);
51}
52
53inline void draw_right_aligned(std::uint64_t value) {
54 std::array<char, std::numeric_limits<std::uint64_t>::digits10 + 2> text{};
55 const auto result =
56 std::to_chars(text.data(), text.data() + text.size() - 1, value);
57 const auto text_width = ImGui::CalcTextSize(text.data(), result.ptr).x;
58 const auto available_width = ImGui::GetContentRegionAvail().x;
59 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + available_width - text_width);
60 ImGui::TextUnformatted(text.data(), result.ptr);
61}
62} // namespace detail
63
65[[nodiscard]] inline auto category_name(
66 diagnostics::TraceCategory category) noexcept -> const char* {
67 switch (category) {
68 case diagnostics::TraceCategory::General:
69 return "General";
70 case diagnostics::TraceCategory::Path:
71 return "Path";
72 case diagnostics::TraceCategory::Topology:
73 return "Topology";
74 case diagnostics::TraceCategory::Queued:
75 return "Queued";
76 case diagnostics::TraceCategory::Planner:
77 return "Planner";
78 case diagnostics::TraceCategory::Scheduler:
79 return "Scheduler";
80 case diagnostics::TraceCategory::Render:
81 return "Render";
82 case diagnostics::TraceCategory::Count:
83 return "Count";
84 }
85 return "?";
86}
87
90inline void draw_timing_panel(const diagnostics::TimingSnapshot& timing) {
91 ImGui::TextUnformatted("Timing (ns)");
92 ImGui::Separator();
93 constexpr int column_count = 6;
94 constexpr ImGuiTableFlags flags =
95 ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings |
96 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
97 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX |
98 ImGuiTableFlags_ScrollY;
99 const auto unit = ImGui::GetTextLineHeightWithSpacing();
100 const auto visible_rows =
101 static_cast<float>(diagnostics::trace_category_count) + 2.0F;
102 if (!ImGui::BeginTable("##tess_timing", column_count, flags,
103 ImVec2{0.0F, unit * visible_rows})) {
104 return;
105 }
106 ImGui::TableSetupColumn("Category", ImGuiTableColumnFlags_WidthFixed,
107 unit * 4.5F);
108 ImGui::TableSetupColumn("Samples", ImGuiTableColumnFlags_WidthFixed,
109 unit * 3.5F);
110 ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthFixed,
111 unit * 5.5F);
112 ImGui::TableSetupColumn("Average", ImGuiTableColumnFlags_WidthFixed,
113 unit * 5.5F);
114 ImGui::TableSetupColumn("Minimum", ImGuiTableColumnFlags_WidthFixed,
115 unit * 5.5F);
116 ImGui::TableSetupColumn("Maximum", ImGuiTableColumnFlags_WidthFixed,
117 unit * 5.5F);
118 ImGui::TableSetupScrollFreeze(1, 1);
119 ImGui::TableHeadersRow();
120 for (std::size_t index = 0; index < diagnostics::trace_category_count;
121 ++index) {
122 const auto category = static_cast<diagnostics::TraceCategory>(index);
123 const auto& stats = timing.stats(category);
124 const auto average =
125 stats.samples == 0 ? std::uint64_t{0} : stats.total_ns / stats.samples;
126 ImGui::TableNextRow();
127 ImGui::TableSetColumnIndex(0);
128 ImGui::TextUnformatted(category_name(category));
129 ImGui::TableSetColumnIndex(1);
130 detail::draw_right_aligned(stats.samples);
131 ImGui::TableSetColumnIndex(2);
132 detail::draw_right_aligned(stats.total_ns);
133 ImGui::TableSetColumnIndex(3);
134 detail::draw_right_aligned(average);
135 ImGui::TableSetColumnIndex(4);
136 detail::draw_right_aligned(stats.min_ns);
137 ImGui::TableSetColumnIndex(5);
138 detail::draw_right_aligned(stats.max_ns);
139 }
140 ImGui::EndTable();
141}
142
144inline void draw_path_counters_panel(const diagnostics::PathCounters& path) {
145 ImGui::TextUnformatted("Path counters");
146 ImGui::Separator();
147 ImGui::Text("heap push / pop: %llu / %llu", detail::to_ull(path.heap_pushes),
148 detail::to_ull(path.heap_pops));
149 ImGui::Text("relax ok / attempts: %llu / %llu",
150 detail::to_ull(path.relax_successes),
151 detail::to_ull(path.relax_attempts));
152 ImGui::Text("touched nodes: %llu", detail::to_ull(path.touched_nodes));
153 ImGui::Text("passability checks: %llu",
154 detail::to_ull(path.passability_checks));
155}
156
158inline void draw_queued_counters_panel(
159 const diagnostics::QueuedPhaseCounters& queued) {
160 ImGui::TextUnformatted("Queued phase counters");
161 ImGui::Separator();
162 ImGui::Text("phase calls / ops: %llu / %llu",
163 detail::to_ull(queued.phase_calls),
164 detail::to_ull(queued.phase_operations));
165 ImGui::Text("failures: %llu", detail::to_ull(queued.phase_failures));
166 ImGui::Text("dirty merged: %llu", detail::to_ull(queued.dirty_chunks_merged));
167}
168
170inline void draw_allocation_counters_panel(
171 const diagnostics::AllocationCounters& allocation) {
172 ImGui::TextUnformatted("Allocation counters");
173 ImGui::Separator();
174 ImGui::Text("alloc: %llu (%llu bytes)",
175 detail::to_ull(allocation.allocations),
176 detail::to_ull(allocation.allocation_bytes));
177 ImGui::Text("free: %llu (%llu bytes)",
178 detail::to_ull(allocation.deallocations),
179 detail::to_ull(allocation.deallocation_bytes));
180 ImGui::Text("live / peak: %llu / %llu bytes",
181 detail::to_ull(allocation.live_bytes),
182 detail::to_ull(allocation.peak_live_bytes));
183}
184
186inline void draw_recent_timing_spans_panel(
187 const diagnostics::DiagnosticsSnapshot& snapshot) {
188 ImGui::TextUnformatted("Recent timed spans");
189 ImGui::Separator();
190 for (std::size_t index = 0; index < snapshot.trace_record_count; ++index) {
191 const auto& record = snapshot.trace_records[index];
192 if (record.kind != diagnostics::TraceRecordKind::Duration) {
193 continue;
194 }
195 const auto label_size =
196 record.label.size() >
197 static_cast<std::size_t>(std::numeric_limits<int>::max())
198 ? std::numeric_limits<int>::max()
199 : static_cast<int>(record.label.size());
200 const auto milliseconds = static_cast<double>(record.value) / 1'000'000.0;
201 const auto* label = record.label.empty() ? "" : record.label.data();
202 ImGui::Text("%-10s %.*s: %.3f ms; alloc/free %llu/%llu bytes",
203 category_name(record.category), label_size, label, milliseconds,
204 detail::to_ull(record.allocation_bytes),
205 detail::to_ull(record.deallocation_bytes));
206 }
207 if (snapshot.trace_records_dropped != 0) {
208 ImGui::Text("dropped: %llu",
209 detail::to_ull(snapshot.trace_records_dropped));
210 }
211}
212
214inline void draw_diagnostics_panel(
215 const diagnostics::DiagnosticsSnapshot& snapshot) {
216 draw_timing_panel(snapshot.timing);
217 ImGui::Separator();
218 draw_recent_timing_spans_panel(snapshot);
219 ImGui::Separator();
220 draw_path_counters_panel(snapshot.path);
221 ImGui::Separator();
222 draw_queued_counters_panel(snapshot.queued);
223 ImGui::Separator();
224 draw_allocation_counters_panel(snapshot.allocation);
225}
226
227} // namespace tess::debug::imgui
228
229#endif // defined(TESS_ENABLE_IMGUI) && TESS_DIAGNOSTICS_ENABLED