| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
|
| 22 |
|
| 23 |
|
| 24 |
|
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
#include "imgui.h" |
| 29 |
#include "debug_ui_panels.h" |
| 30 |
#include "debug_ui_bridge.h" |
| 31 |
#include "debug_ui_widgets.h" |
| 32 |
#include "debug_ui_settings.h" |
| 33 |
|
| 34 |
#include <stdint.h> |
| 35 |
#include <stdio.h> |
| 36 |
#include <string.h> |
| 37 |
#include <math.h> |
| 38 |
|
| 39 |
namespace { |
| 40 |
|
| 41 |
|
| 42 |
|
| 43 |
struct Window { |
| 44 |
const char *name; |
| 45 |
float seconds; |
| 46 |
}; |
| 47 |
const Window k_windows[] = { |
| 48 |
{"1s", 1.0f}, {"10s", 10.0f}, {"30s", 30.0f}, {"60s", 60.0f}, {"session", 0.0f}, |
| 49 |
}; |
| 50 |
const int k_window_count = (int)(sizeof k_windows / sizeof k_windows[0]); |
| 51 |
|
| 52 |
|
| 53 |
float g_graph[1024]; |
| 54 |
int g_graph_n = 0; |
| 55 |
|
| 56 |
|
| 57 |
char g_zone_name[PERF_ZONE_NAME_MAX] = ""; |
| 58 |
int g_zone_start = 0x0800; |
| 59 |
int g_zone_end = 0x0900; |
| 60 |
int g_zone_bank = -1; |
| 61 |
bool g_zone_idle = false; |
| 62 |
int g_symbol_pick = -1; |
| 63 |
|
| 64 |
ImVec4 |
| 65 |
utilization_color(float pct) |
| 66 |
{ |
| 67 |
if (pct >= 100.0f) |
| 68 |
return ImVec4(1.00f, 0.35f, 0.35f, 1.0f); |
| 69 |
if (pct >= 90.0f) |
| 70 |
return ImVec4(1.00f, 0.75f, 0.30f, 1.0f); |
| 71 |
return ImVec4(0.45f, 0.90f, 0.50f, 1.0f); |
| 72 |
} |
| 73 |
|
| 74 |
|
| 75 |
|
| 76 |
void |
| 77 |
cycles_tooltip(uint32_t cycles) |
| 78 |
{ |
| 79 |
if (!ImGui::IsItemHovered()) |
| 80 |
return; |
| 81 |
const uint32_t per_line = perf_budget_cycles_per_scanline(); |
| 82 |
ImGui::BeginTooltip(); |
| 83 |
ImGui::Text("%u cycles", (unsigned)cycles); |
| 84 |
if (per_line) |
| 85 |
ImGui::Text("%.1f scanlines", (double)cycles / (double)per_line); |
| 86 |
const uint32_t khz = (uint32_t)MHZ * 1000u; |
| 87 |
if (khz) |
| 88 |
ImGui::Text("%.3f ms", (double)cycles / (double)khz); |
| 89 |
ImGui::EndTooltip(); |
| 90 |
} |
| 91 |
|
| 92 |
void |
| 93 |
draw_budget_header(void) |
| 94 |
{ |
| 95 |
DebugUiSettings &s = debug_ui_settings(); |
| 96 |
|
| 97 |
const uint32_t per_frame = perf_budget_cycles_per_frame(); |
| 98 |
const uint32_t budget = perf_budget_budget_cycles(); |
| 99 |
const int span = perf_budget_frames_per_budget(); |
| 100 |
|
| 101 |
ImGui::SetNextItemWidth(dbgui_field_width("000.0")); |
| 102 |
if (ImGui::InputFloat("Target FPS", &s.perf_target_fps, 0.0f, 0.0f, "%.4g")) { |
| 103 |
if (s.perf_target_fps < 1.0f) |
| 104 |
s.perf_target_fps = 1.0f; |
| 105 |
if (s.perf_target_fps > 1000.0f) |
| 106 |
s.perf_target_fps = 1000.0f; |
| 107 |
perf_budget_set_target_fps(s.perf_target_fps); |
| 108 |
debug_ui_settings_mark_dirty(); |
| 109 |
} |
| 110 |
if (ImGui::IsItemHovered()) { |
| 111 |
ImGui::SetTooltip( |
| 112 |
"How many vsyncs your frame work is allowed to span.\n\n" |
| 113 |
"The frame period is fixed by VERA and asking for a lower rate\n" |
| 114 |
"does not make frames longer -- it means the work may take more\n" |
| 115 |
"than one of them, so the budget grows instead."); |
| 116 |
} |
| 117 |
ImGui::SameLine(); |
| 118 |
for (int i = 0; i < 3; i++) { |
| 119 |
const float presets[3] = {60.0f, 30.0f, 20.0f}; |
| 120 |
char label[16]; |
| 121 |
snprintf(label, sizeof label, "%g", (double)presets[i]); |
| 122 |
if (i) |
| 123 |
ImGui::SameLine(); |
| 124 |
if (ImGui::SmallButton(label)) { |
| 125 |
s.perf_target_fps = presets[i]; |
| 126 |
perf_budget_set_target_fps(presets[i]); |
| 127 |
debug_ui_settings_mark_dirty(); |
| 128 |
} |
| 129 |
} |
| 130 |
|
| 131 |
ImGui::Text("Machine %u MHz | vsync %.2f Hz | %u cycles/frame | %u/scanline", |
| 132 |
(unsigned)MHZ, (double)perf_budget_vsync_hz(), (unsigned)per_frame, |
| 133 |
(unsigned)perf_budget_cycles_per_scanline()); |
| 134 |
ImGui::Text("Budget: %u cycles", (unsigned)budget); |
| 135 |
cycles_tooltip(budget); |
| 136 |
if (span > 1) { |
| 137 |
ImGui::SameLine(); |
| 138 |
ImGui::TextDisabled("(%d frames' worth)", span); |
| 139 |
} |
| 140 |
|
| 141 |
if (warp_mode) { |
| 142 |
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), |
| 143 |
"Warp mode: guest cycles are still real, host time is not."); |
| 144 |
} |
| 145 |
if (timing_get_speed_khz() != timing_native_khz()) { |
| 146 |
ImGui::TextDisabled("Speed override active (%d kHz). The budget follows the " |
| 147 |
"machine's native %d kHz.", |
| 148 |
timing_get_speed_khz(), timing_native_khz()); |
| 149 |
} |
| 150 |
} |
| 151 |
|
| 152 |
void |
| 153 |
draw_current_frame(void) |
| 154 |
{ |
| 155 |
perf_frame_t f; |
| 156 |
if (!perf_budget_last_frame(&f)) { |
| 157 |
ImGui::TextDisabled("(waiting for the first frame)"); |
| 158 |
return; |
| 159 |
} |
| 160 |
|
| 161 |
const uint32_t budget = perf_budget_budget_cycles(); |
| 162 |
const uint32_t work = f.period_work; |
| 163 |
const float pct = budget ? 100.0f * (float)work / (float)budget : 0.0f; |
| 164 |
|
| 165 |
|
| 166 |
|
| 167 |
|
| 168 |
const float full = ImGui::GetContentRegionAvail().x; |
| 169 |
const float height = ImGui::GetFrameHeight(); |
| 170 |
const ImVec2 p0 = ImGui::GetCursorScreenPos(); |
| 171 |
ImDrawList *dl = ImGui::GetWindowDrawList(); |
| 172 |
|
| 173 |
const float scale = budget ? full / (float)budget : 0.0f; |
| 174 |
const float w_irq = (float)f.irq * scale; |
| 175 |
const float w_work = (float)work * scale; |
| 176 |
const float w_total = (float)f.total * scale; |
| 177 |
|
| 178 |
dl->AddRectFilled(p0, ImVec2(p0.x + full, p0.y + height), |
| 179 |
ImGui::GetColorU32(ImGuiCol_FrameBg)); |
| 180 |
|
| 181 |
dl->AddRectFilled(p0, ImVec2(p0.x + (w_total < full ? w_total : full), p0.y + height), |
| 182 |
ImGui::GetColorU32(ImVec4(0.25f, 0.30f, 0.38f, 1.0f))); |
| 183 |
dl->AddRectFilled(p0, ImVec2(p0.x + (w_work < full ? w_work : full), p0.y + height), |
| 184 |
ImGui::GetColorU32(utilization_color(pct))); |
| 185 |
dl->AddRectFilled(p0, ImVec2(p0.x + (w_irq < full ? w_irq : full), p0.y + height), |
| 186 |
ImGui::GetColorU32(ImVec4(0.55f, 0.45f, 0.85f, 1.0f))); |
| 187 |
|
| 188 |
dl->AddLine(ImVec2(p0.x + full - 1.0f, p0.y), ImVec2(p0.x + full - 1.0f, p0.y + height), |
| 189 |
ImGui::GetColorU32(ImVec4(1, 1, 1, 0.7f)), 2.0f); |
| 190 |
|
| 191 |
char overlay[96]; |
| 192 |
snprintf(overlay, sizeof overlay, "%.1f%% of budget %u / %u cycles", (double)pct, |
| 193 |
(unsigned)work, (unsigned)budget); |
| 194 |
const ImVec2 ts = ImGui::CalcTextSize(overlay); |
| 195 |
dl->AddText(ImVec2(p0.x + (full - ts.x) * 0.5f, p0.y + (height - ts.y) * 0.5f), |
| 196 |
ImGui::GetColorU32(ImGuiCol_Text), overlay); |
| 197 |
ImGui::Dummy(ImVec2(full, height)); |
| 198 |
|
| 199 |
const uint32_t idle = f.idle; |
| 200 |
const uint32_t main_cy = work > f.irq ? work - f.irq : 0; |
| 201 |
const uint32_t per_line = perf_budget_cycles_per_scanline(); |
| 202 |
|
| 203 |
ImGui::Text("work %u", (unsigned)work); |
| 204 |
cycles_tooltip(work); |
| 205 |
ImGui::SameLine(); |
| 206 |
ImGui::TextColored(ImVec4(0.55f, 0.45f, 0.85f, 1.0f), " | irq %u", (unsigned)f.irq); |
| 207 |
cycles_tooltip(f.irq); |
| 208 |
ImGui::SameLine(); |
| 209 |
ImGui::Text(" | main %u", (unsigned)main_cy); |
| 210 |
cycles_tooltip(main_cy); |
| 211 |
ImGui::SameLine(); |
| 212 |
ImGui::TextColored(ImVec4(0.55f, 0.62f, 0.72f, 1.0f), " | idle %u", (unsigned)idle); |
| 213 |
cycles_tooltip(idle); |
| 214 |
|
| 215 |
if (work < budget) { |
| 216 |
const uint32_t spare = budget - work; |
| 217 |
ImGui::TextColored(utilization_color(pct), "Headroom: %u cycles%s", (unsigned)spare, |
| 218 |
per_line ? "" : ""); |
| 219 |
if (per_line) { |
| 220 |
ImGui::SameLine(); |
| 221 |
ImGui::TextDisabled("(%.1f scanlines)", (double)spare / (double)per_line); |
| 222 |
} |
| 223 |
} else { |
| 224 |
ImGui::TextColored(utilization_color(pct), "OVER BUDGET by %u cycles", |
| 225 |
(unsigned)(work - budget)); |
| 226 |
if (per_line) { |
| 227 |
ImGui::SameLine(); |
| 228 |
ImGui::TextDisabled("(%.1f scanlines)", |
| 229 |
(double)(work - budget) / (double)per_line); |
| 230 |
} |
| 231 |
} |
| 232 |
ImGui::Text("Instructions: %u | host %.2f ms", (unsigned)f.instructions, |
| 233 |
(double)f.host_us / 1000.0); |
| 234 |
} |
| 235 |
|
| 236 |
void |
| 237 |
draw_graph(void) |
| 238 |
{ |
| 239 |
DebugUiSettings &s = debug_ui_settings(); |
| 240 |
|
| 241 |
int want = s.perf_graph_frames; |
| 242 |
if (want < 30) |
| 243 |
want = 30; |
| 244 |
if (want > (int)(sizeof g_graph / sizeof g_graph[0])) |
| 245 |
want = (int)(sizeof g_graph / sizeof g_graph[0]); |
| 246 |
|
| 247 |
|
| 248 |
|
| 249 |
const int have = perf_budget_frame_count(); |
| 250 |
int n = want < have ? want : have; |
| 251 |
if (n <= 0) { |
| 252 |
ImGui::TextDisabled("(no frames yet)"); |
| 253 |
return; |
| 254 |
} |
| 255 |
|
| 256 |
perf_frame_t f; |
| 257 |
g_graph_n = 0; |
| 258 |
|
| 259 |
for (int i = n - 1; i >= 0; i--) { |
| 260 |
if (perf_budget_frame_at(i, &f)) |
| 261 |
g_graph[g_graph_n++] = (float)f.period_work; |
| 262 |
} |
| 263 |
if (g_graph_n <= 0) { |
| 264 |
ImGui::TextDisabled("(no frames yet)"); |
| 265 |
return; |
| 266 |
} |
| 267 |
|
| 268 |
const float budget = (float)perf_budget_budget_cycles(); |
| 269 |
float top = budget * 1.25f; |
| 270 |
for (int i = 0; i < g_graph_n; i++) |
| 271 |
if (g_graph[i] > top) |
| 272 |
top = g_graph[i] * 1.05f; |
| 273 |
|
| 274 |
const float height = ImGui::GetTextLineHeight() * 6.0f; |
| 275 |
const ImVec2 p0 = ImGui::GetCursorScreenPos(); |
| 276 |
const float width = ImGui::GetContentRegionAvail().x; |
| 277 |
|
| 278 |
ImGui::PlotHistogram("##perfgraph", g_graph, g_graph_n, 0, nullptr, 0.0f, top, |
| 279 |
ImVec2(width, height)); |
| 280 |
|
| 281 |
|
| 282 |
|
| 283 |
if (top > 0.0f) { |
| 284 |
const float y = p0.y + height * (1.0f - budget / top); |
| 285 |
ImGui::GetWindowDrawList()->AddLine(ImVec2(p0.x, y), ImVec2(p0.x + width, y), |
| 286 |
ImGui::GetColorU32(ImVec4(1.0f, 0.4f, 0.4f, 0.9f)), |
| 287 |
1.5f); |
| 288 |
} |
| 289 |
ImGui::TextDisabled("%d frames, oldest left. Red line is the budget.", g_graph_n); |
| 290 |
} |
| 291 |
|
| 292 |
void |
| 293 |
stat_cell(uint32_t v, uint32_t budget) |
| 294 |
{ |
| 295 |
const float pct = budget ? 100.0f * (float)v / (float)budget : 0.0f; |
| 296 |
ImGui::TextColored(utilization_color(pct), "%u", (unsigned)v); |
| 297 |
if (ImGui::IsItemHovered()) { |
| 298 |
ImGui::BeginTooltip(); |
| 299 |
ImGui::Text("%u cycles (%.1f%% of budget)", (unsigned)v, (double)pct); |
| 300 |
const uint32_t per_line = perf_budget_cycles_per_scanline(); |
| 301 |
if (per_line) |
| 302 |
ImGui::Text("%.1f scanlines", (double)v / (double)per_line); |
| 303 |
ImGui::EndTooltip(); |
| 304 |
} |
| 305 |
} |
| 306 |
|
| 307 |
void |
| 308 |
draw_statistics(void) |
| 309 |
{ |
| 310 |
const uint32_t budget = perf_budget_budget_cycles(); |
| 311 |
|
| 312 |
if (!ImGui::BeginTable("perfstats", 9, |
| 313 |
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | |
| 314 |
DBGUI_TABLE_FLAGS_RESIZABLE)) |
| 315 |
return; |
| 316 |
|
| 317 |
ImGui::TableSetupColumn("Window", ImGuiTableColumnFlags_WidthFixed, |
| 318 |
dbgui_col_width("Window", "session")); |
| 319 |
ImGui::TableSetupColumn("Frames", ImGuiTableColumnFlags_WidthFixed, |
| 320 |
dbgui_col_width("Frames", "99999")); |
| 321 |
ImGui::TableSetupColumn("Min", ImGuiTableColumnFlags_WidthStretch); |
| 322 |
ImGui::TableSetupColumn("Mean", ImGuiTableColumnFlags_WidthStretch); |
| 323 |
ImGui::TableSetupColumn("p50", ImGuiTableColumnFlags_WidthStretch); |
| 324 |
ImGui::TableSetupColumn("p95", ImGuiTableColumnFlags_WidthStretch); |
| 325 |
ImGui::TableSetupColumn("p99", ImGuiTableColumnFlags_WidthStretch); |
| 326 |
ImGui::TableSetupColumn("Max", ImGuiTableColumnFlags_WidthStretch); |
| 327 |
ImGui::TableSetupColumn("Over", ImGuiTableColumnFlags_WidthFixed, |
| 328 |
dbgui_col_width("Over", "9999 (99%)")); |
| 329 |
ImGui::TableHeadersRow(); |
| 330 |
|
| 331 |
for (int i = 0; i < k_window_count; i++) { |
| 332 |
perf_window_t w; |
| 333 |
if (!perf_budget_window(k_windows[i].seconds, &w)) |
| 334 |
continue; |
| 335 |
|
| 336 |
ImGui::TableNextRow(); |
| 337 |
ImGui::TableSetColumnIndex(0); |
| 338 |
ImGui::TextUnformatted(k_windows[i].name); |
| 339 |
ImGui::TableSetColumnIndex(1); |
| 340 |
ImGui::Text("%d", w.frames); |
| 341 |
|
| 342 |
ImGui::TableSetColumnIndex(2); stat_cell(w.budget_work.min, budget); |
| 343 |
ImGui::TableSetColumnIndex(3); stat_cell(w.budget_work.mean, budget); |
| 344 |
ImGui::TableSetColumnIndex(4); stat_cell(w.budget_work.p50, budget); |
| 345 |
ImGui::TableSetColumnIndex(5); stat_cell(w.budget_work.p95, budget); |
| 346 |
ImGui::TableSetColumnIndex(6); stat_cell(w.budget_work.p99, budget); |
| 347 |
ImGui::TableSetColumnIndex(7); stat_cell(w.budget_work.max, budget); |
| 348 |
|
| 349 |
ImGui::TableSetColumnIndex(8); |
| 350 |
if (w.overruns > 0) { |
| 351 |
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%d (%.1f%%)", w.overruns, |
| 352 |
(double)w.overrun_pct); |
| 353 |
if (ImGui::IsItemHovered()) |
| 354 |
ImGui::SetTooltip("Worst was frame %u, %u cycles over budget.", |
| 355 |
(unsigned)w.worst_frame, (unsigned)w.worst_overrun); |
| 356 |
} else { |
| 357 |
ImGui::TextDisabled("-"); |
| 358 |
} |
| 359 |
} |
| 360 |
ImGui::EndTable(); |
| 361 |
ImGui::TextDisabled("Work per budget period, in guest cycles. " |
| 362 |
"Colour is share of budget."); |
| 363 |
} |
| 364 |
|
| 365 |
void |
| 366 |
draw_breakdown(void) |
| 367 |
{ |
| 368 |
const uint32_t budget = perf_budget_budget_cycles(); |
| 369 |
const int nzones = perf_budget_zone_count(); |
| 370 |
if (nzones <= 0) { |
| 371 |
ImGui::TextDisabled("No zones or marker regions yet. Define a zone below, or have " |
| 372 |
"the guest write region markers to $9FBC."); |
| 373 |
return; |
| 374 |
} |
| 375 |
|
| 376 |
if (!ImGui::BeginTable("perfzones", 6, |
| 377 |
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | |
| 378 |
DBGUI_TABLE_FLAGS_RESIZABLE)) |
| 379 |
return; |
| 380 |
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthStretch); |
| 381 |
ImGui::TableSetupColumn("Range", ImGuiTableColumnFlags_WidthFixed, |
| 382 |
dbgui_col_width("Range", "$FFFF-$FFFF")); |
| 383 |
ImGui::TableSetupColumn("Now", ImGuiTableColumnFlags_WidthStretch); |
| 384 |
ImGui::TableSetupColumn("Mean 30s", ImGuiTableColumnFlags_WidthStretch); |
| 385 |
ImGui::TableSetupColumn("p95 30s", ImGuiTableColumnFlags_WidthStretch); |
| 386 |
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, |
| 387 |
dbgui_col_width("", "Remove")); |
| 388 |
ImGui::TableHeadersRow(); |
| 389 |
|
| 390 |
perf_frame_t last; |
| 391 |
const bool have_last = perf_budget_last_frame(&last); |
| 392 |
|
| 393 |
for (int i = 0; i < nzones; i++) { |
| 394 |
perf_zone_t z; |
| 395 |
if (!perf_budget_zone_get(i, &z)) |
| 396 |
continue; |
| 397 |
ImGui::TableNextRow(); |
| 398 |
ImGui::PushID(i); |
| 399 |
|
| 400 |
ImGui::TableSetColumnIndex(0); |
| 401 |
ImGui::TextUnformatted(z.name); |
| 402 |
if (z.is_idle) { |
| 403 |
ImGui::SameLine(); |
| 404 |
ImGui::TextDisabled("(idle)"); |
| 405 |
} |
| 406 |
|
| 407 |
ImGui::TableSetColumnIndex(1); |
| 408 |
if (z.is_marker) |
| 409 |
ImGui::TextDisabled("marker"); |
| 410 |
else if (z.bank >= 0) |
| 411 |
ImGui::Text("$%04X-$%04X b%d", z.start, z.end, z.bank); |
| 412 |
else |
| 413 |
ImGui::Text("$%04X-$%04X", z.start, z.end); |
| 414 |
|
| 415 |
ImGui::TableSetColumnIndex(2); |
| 416 |
if (have_last && i < PERF_RING_ZONES) |
| 417 |
stat_cell(last.zone[i], budget); |
| 418 |
else |
| 419 |
ImGui::TextDisabled("-"); |
| 420 |
|
| 421 |
perf_stat_t st; |
| 422 |
const bool have_stat = perf_budget_zone_window(i, 30.0f, &st); |
| 423 |
ImGui::TableSetColumnIndex(3); |
| 424 |
if (have_stat) |
| 425 |
stat_cell(st.mean, budget); |
| 426 |
else |
| 427 |
ImGui::TextDisabled("-"); |
| 428 |
ImGui::TableSetColumnIndex(4); |
| 429 |
if (have_stat) |
| 430 |
stat_cell(st.p95, budget); |
| 431 |
else |
| 432 |
ImGui::TextDisabled("-"); |
| 433 |
|
| 434 |
ImGui::TableSetColumnIndex(5); |
| 435 |
if (ImGui::SmallButton("Remove")) |
| 436 |
perf_budget_zone_remove(i); |
| 437 |
|
| 438 |
ImGui::PopID(); |
| 439 |
} |
| 440 |
ImGui::EndTable(); |
| 441 |
|
| 442 |
if (nzones > PERF_RING_ZONES) { |
| 443 |
ImGui::TextDisabled("Only the first %d zones keep per-frame history; the rest " |
| 444 |
"report live figures only.", PERF_RING_ZONES); |
| 445 |
} |
| 446 |
} |
| 447 |
|
| 448 |
void |
| 449 |
draw_zone_editor(void) |
| 450 |
{ |
| 451 |
ImGui::SetNextItemWidth(dbgui_field_width("a_reasonably_long_name")); |
| 452 |
ImGui::InputText("Name", g_zone_name, sizeof g_zone_name); |
| 453 |
|
| 454 |
ImGui::SetNextItemWidth(dbgui_field_width("$FFFF")); |
| 455 |
ImGui::InputInt("Start", &g_zone_start, 0, 0, ImGuiInputTextFlags_CharsHexadecimal); |
| 456 |
ImGui::SameLine(); |
| 457 |
ImGui::SetNextItemWidth(dbgui_field_width("$FFFF")); |
| 458 |
ImGui::InputInt("End", &g_zone_end, 0, 0, ImGuiInputTextFlags_CharsHexadecimal); |
| 459 |
ImGui::SameLine(); |
| 460 |
ImGui::SetNextItemWidth(dbgui_field_width("-1")); |
| 461 |
ImGui::InputInt("Bank", &g_zone_bank, 0, 0); |
| 462 |
if (ImGui::IsItemHovered()) |
| 463 |
ImGui::SetTooltip("-1 matches any bank."); |
| 464 |
ImGui::SameLine(); |
| 465 |
ImGui::Checkbox("Idle", &g_zone_idle); |
| 466 |
if (ImGui::IsItemHovered()) { |
| 467 |
ImGui::SetTooltip("Cycles in this range are WAITING, not work.\n" |
| 468 |
"Use this for a wait loop the automatic detector cannot see --\n" |
| 469 |
"one that stores to memory, or calls a subroutine."); |
| 470 |
} |
| 471 |
|
| 472 |
|
| 473 |
|
| 474 |
|
| 475 |
if (dbg_info_is_loaded()) { |
| 476 |
const int nsym = dbg_info_symbol_count(); |
| 477 |
if (ImGui::Button("From symbol...")) { |
| 478 |
ImGui::OpenPopup("perf_symbol_pick"); |
| 479 |
g_symbol_pick = -1; |
| 480 |
} |
| 481 |
if (ImGui::BeginPopup("perf_symbol_pick")) { |
| 482 |
static char filter[64] = ""; |
| 483 |
ImGui::SetNextItemWidth(dbgui_field_width("a_reasonably_long_name")); |
| 484 |
ImGui::InputText("Filter", filter, sizeof filter); |
| 485 |
ImGui::BeginChild("perf_symbol_list", ImVec2(320, 240)); |
| 486 |
for (int i = 0; i < nsym; i++) { |
| 487 |
const char *name = nullptr; |
| 488 |
dbg_addr_t addr = 0; |
| 489 |
if (!dbg_info_symbol_at(i, &name, &addr) || !name) |
| 490 |
continue; |
| 491 |
if (filter[0] && !strstr(name, filter)) |
| 492 |
continue; |
| 493 |
char label[96]; |
| 494 |
snprintf(label, sizeof label, "%s $%04X", name, (unsigned)(addr & 0xFFFF)); |
| 495 |
if (ImGui::Selectable(label)) { |
| 496 |
|
| 497 |
|
| 498 |
|
| 499 |
dbg_addr_t next = 0x10000; |
| 500 |
for (int j = 0; j < nsym; j++) { |
| 501 |
const char *n2 = nullptr; |
| 502 |
dbg_addr_t a2 = 0; |
| 503 |
if (dbg_info_symbol_at(j, &n2, &a2) && a2 > addr && a2 < next) |
| 504 |
next = a2; |
| 505 |
} |
| 506 |
snprintf(g_zone_name, sizeof g_zone_name, "%s", name); |
| 507 |
g_zone_start = (int)(addr & 0xFFFF); |
| 508 |
g_zone_end = (int)(next > 0xFFFF ? 0x10000 : next); |
| 509 |
ImGui::CloseCurrentPopup(); |
| 510 |
} |
| 511 |
} |
| 512 |
ImGui::EndChild(); |
| 513 |
ImGui::EndPopup(); |
| 514 |
} |
| 515 |
ImGui::SameLine(); |
| 516 |
} |
| 517 |
|
| 518 |
const bool valid = g_zone_end > g_zone_start && g_zone_start >= 0 && g_zone_end <= 0x10000; |
| 519 |
ImGui::BeginDisabled(!valid); |
| 520 |
if (ImGui::Button("Add zone")) { |
| 521 |
perf_budget_zone_add(g_zone_name[0] ? g_zone_name : "zone", (uint16_t)g_zone_start, |
| 522 |
(uint16_t)(g_zone_end > 0xFFFF ? 0xFFFF : g_zone_end), |
| 523 |
(int16_t)g_zone_bank, g_zone_idle); |
| 524 |
g_zone_name[0] = '\0'; |
| 525 |
} |
| 526 |
ImGui::EndDisabled(); |
| 527 |
if (!valid) { |
| 528 |
ImGui::SameLine(); |
| 529 |
ImGui::TextDisabled("(end must be above start)"); |
| 530 |
} |
| 531 |
ImGui::SameLine(); |
| 532 |
if (ImGui::Button("Clear all")) |
| 533 |
perf_budget_zone_clear(); |
| 534 |
} |
| 535 |
|
| 536 |
void |
| 537 |
draw_controls(void) |
| 538 |
{ |
| 539 |
DebugUiSettings &s = debug_ui_settings(); |
| 540 |
|
| 541 |
if (ImGui::Button("Reset statistics")) |
| 542 |
perf_budget_reset(); |
| 543 |
ImGui::SameLine(); |
| 544 |
|
| 545 |
const char *modes[] = {"Auto (WAI + spin detection)", "Markers and idle zones only", |
| 546 |
"Nothing is idle"}; |
| 547 |
ImGui::SetNextItemWidth(dbgui_combo_width("Auto (WAI + spin detection)")); |
| 548 |
if (ImGui::Combo("Idle detection", &s.perf_idle_mode, modes, 3)) { |
| 549 |
perf_budget_set_idle_mode((perf_idle_mode_t)s.perf_idle_mode); |
| 550 |
debug_ui_settings_mark_dirty(); |
| 551 |
} |
| 552 |
if (ImGui::IsItemHovered()) { |
| 553 |
ImGui::SetTooltip( |
| 554 |
"How waiting is told apart from work.\n\n" |
| 555 |
"Auto catches WAI and a tight loop that stores nothing, which covers\n" |
| 556 |
"polling $9F27 and polling a flag an IRQ handler sets. It CANNOT see\n" |
| 557 |
"a wait loop that stores or calls a subroutine -- mark those as an\n" |
| 558 |
"idle zone, or bracket the real work with $9FBC markers."); |
| 559 |
} |
| 560 |
|
| 561 |
if (ImGui::Checkbox("Keep profiling when this panel is closed", &s.perf_always_on)) |
| 562 |
debug_ui_settings_mark_dirty(); |
| 563 |
if (ImGui::IsItemHovered()) { |
| 564 |
ImGui::SetTooltip("Profiling costs the running machine an add and a table lookup\n" |
| 565 |
"per instruction, so it is normally armed only while this panel\n" |
| 566 |
"is open. Turn this on to keep a DAP client fed with the panel shut."); |
| 567 |
} |
| 568 |
|
| 569 |
ImGui::SetNextItemWidth(dbgui_field_width("99999")); |
| 570 |
if (ImGui::SliderInt("History (frames)", &s.perf_capacity, 120, 7200)) { |
| 571 |
perf_budget_set_capacity(s.perf_capacity); |
| 572 |
debug_ui_settings_mark_dirty(); |
| 573 |
} |
| 574 |
ImGui::SameLine(); |
| 575 |
ImGui::SetNextItemWidth(dbgui_field_width("99999")); |
| 576 |
if (ImGui::SliderInt("Graph frames", &s.perf_graph_frames, 30, 1024)) |
| 577 |
debug_ui_settings_mark_dirty(); |
| 578 |
} |
| 579 |
|
| 580 |
|
| 581 |
|
| 582 |
|
| 583 |
|
| 584 |
|
| 585 |
|
| 586 |
|
| 587 |
|
| 588 |
|
| 589 |
|
| 590 |
|
| 591 |
|
| 592 |
|
| 593 |
void |
| 594 |
draw_bandwidth(void) |
| 595 |
{ |
| 596 |
vera_bw_frame_t f; |
| 597 |
if (!vera_bandwidth_last_frame(&f)) { |
| 598 |
ImGui::TextDisabled("No completed frame yet."); |
| 599 |
return; |
| 600 |
} |
| 601 |
|
| 602 |
const float peak_pct = 100.0f * (float)f.peak_clocks / (float)VERA_BW_LINE_CLOCKS; |
| 603 |
|
| 604 |
ImGui::Text("Peak scanline:"); |
| 605 |
ImGui::SameLine(); |
| 606 |
ImGui::TextColored(utilization_color(peak_pct), "%u of %d clocks (%.1f%%)", |
| 607 |
(unsigned)f.peak_clocks, VERA_BW_LINE_CLOCKS, peak_pct); |
| 608 |
ImGui::SameLine(); |
| 609 |
ImGui::TextDisabled("on line %u", (unsigned)f.peak_line); |
| 610 |
|
| 611 |
ImGui::Text("p95 %u mean %u over budget: %u line(s)", (unsigned)f.p95_clocks, |
| 612 |
(unsigned)f.mean_clocks, (unsigned)f.lines_over); |
| 613 |
|
| 614 |
|
| 615 |
if (ImGui::BeginTable("bw_layers", 4, |
| 616 |
ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) { |
| 617 |
ImGui::TableSetupColumn("Source"); |
| 618 |
ImGui::TableSetupColumn("Fetches/frame"); |
| 619 |
ImGui::TableSetupColumn("Bytes/frame"); |
| 620 |
ImGui::TableSetupColumn("Peak line"); |
| 621 |
ImGui::TableHeadersRow(); |
| 622 |
|
| 623 |
for (int i = 0; i < 2; i++) { |
| 624 |
ImGui::TableNextRow(); |
| 625 |
ImGui::TableNextColumn(); |
| 626 |
ImGui::Text("Layer %d", i); |
| 627 |
ImGui::TableNextColumn(); |
| 628 |
ImGui::Text("%u", (unsigned)f.layer_fetches[i]); |
| 629 |
ImGui::TableNextColumn(); |
| 630 |
ImGui::Text("%u", (unsigned)f.layer_bytes[i]); |
| 631 |
ImGui::TableNextColumn(); |
| 632 |
ImGui::Text("%u on line %u", (unsigned)f.layer_peak_fetches[i], |
| 633 |
(unsigned)f.layer_peak_line[i]); |
| 634 |
} |
| 635 |
|
| 636 |
ImGui::TableNextRow(); |
| 637 |
ImGui::TableNextColumn(); |
| 638 |
ImGui::TextUnformatted("CPU port"); |
| 639 |
ImGui::TableNextColumn(); |
| 640 |
ImGui::Text("%u", (unsigned)f.port_accesses); |
| 641 |
ImGui::TableNextColumn(); |
| 642 |
ImGui::Text("%u", (unsigned)f.port_vram_bytes); |
| 643 |
ImGui::TableNextColumn(); |
| 644 |
ImGui::TextDisabled("-"); |
| 645 |
|
| 646 |
ImGui::EndTable(); |
| 647 |
} |
| 648 |
|
| 649 |
|
| 650 |
ImGui::Spacing(); |
| 651 |
const uint32_t moved = f.port_reads + f.port_writes; |
| 652 |
ImGui::Text("Data port ($9F23/$9F24): %u bytes written, %u read", (unsigned)f.port_writes, |
| 653 |
(unsigned)f.port_reads); |
| 654 |
if (ImGui::IsItemHovered()) { |
| 655 |
ImGui::SetTooltip("An 8 MHz CPU can issue roughly 64 accesses a scanline\n" |
| 656 |
"(a 4-cycle sta is 12.5 VERA clocks), so about 33 KB a\n" |
| 657 |
"frame is the ceiling no program can push past."); |
| 658 |
} |
| 659 |
if (f.fx_cache_writes || f.fx_affine_fetches) { |
| 660 |
const double amp = moved ? (double)f.port_vram_bytes / (double)moved : 0.0; |
| 661 |
ImGui::Text("VERA FX: %u cache write(s), %u affine fetch(es) — %.2fx amplification", |
| 662 |
(unsigned)f.fx_cache_writes, (unsigned)f.fx_affine_fetches, amp); |
| 663 |
if (ImGui::IsItemHovered()) { |
| 664 |
ImGui::SetTooltip("Bytes that reached VRAM per byte the CPU pushed.\n" |
| 665 |
"An FX cache write moves four bytes for one store,\n" |
| 666 |
"which is the whole reason to use it."); |
| 667 |
} |
| 668 |
} |
| 669 |
|
| 670 |
|
| 671 |
uint16_t nlines = 0; |
| 672 |
const vera_bw_line_t *lines = vera_bandwidth_last_lines(&nlines); |
| 673 |
if (!lines || !nlines) |
| 674 |
return; |
| 675 |
|
| 676 |
ImGui::Spacing(); |
| 677 |
ImGui::TextDisabled("Height is the bus time a scanline wanted. Anything above the white " |
| 678 |
"line is more than the scanline has."); |
| 679 |
|
| 680 |
ImDrawList *dl = ImGui::GetWindowDrawList(); |
| 681 |
const ImVec2 origin = ImGui::GetCursorScreenPos(); |
| 682 |
const float w = ImGui::GetContentRegionAvail().x; |
| 683 |
const float h = 180.0f; |
| 684 |
ImGui::InvisibleButton("bw_lines", ImVec2(w, h)); |
| 685 |
const bool hovered = ImGui::IsItemHovered(); |
| 686 |
|
| 687 |
dl->AddRectFilled(origin, ImVec2(origin.x + w, origin.y + h), IM_COL32(18, 18, 22, 255)); |
| 688 |
|
| 689 |
|
| 690 |
|
| 691 |
|
| 692 |
const float floor_top = (float)VERA_BW_LINE_CLOCKS * 4.0f / 3.0f; |
| 693 |
float top = (float)f.peak_clocks > floor_top ? (float)f.peak_clocks : floor_top; |
| 694 |
const float bw = w / (float)nlines; |
| 695 |
|
| 696 |
for (uint16_t line = 0; line < nlines; ++line) { |
| 697 |
const vera_bw_line_t &st = lines[line]; |
| 698 |
const uint32_t clocks = vera_bw_line_clocks(&st); |
| 699 |
if (!clocks) |
| 700 |
continue; |
| 701 |
const float bh = ((float)clocks / top) * h; |
| 702 |
const ImU32 col = clocks > VERA_BW_LINE_CLOCKS ? IM_COL32(235, 70, 60, 255) |
| 703 |
: IM_COL32(120, 200, 140, 255); |
| 704 |
dl->AddRectFilled(ImVec2(origin.x + line * bw, origin.y + h - bh), |
| 705 |
ImVec2(origin.x + line * bw + (bw > 1.0f ? bw : 1.0f), origin.y + h), |
| 706 |
col); |
| 707 |
} |
| 708 |
|
| 709 |
const float ceil_y = origin.y + h - ((float)VERA_BW_LINE_CLOCKS / top) * h; |
| 710 |
dl->AddLine(ImVec2(origin.x, ceil_y), ImVec2(origin.x + w, ceil_y), |
| 711 |
IM_COL32(255, 255, 255, 200)); |
| 712 |
dl->AddText(ImVec2(origin.x + 4, ceil_y - 16), IM_COL32(255, 255, 255, 200), |
| 713 |
"one scanline (800 clocks)"); |
| 714 |
|
| 715 |
if (hovered && bw > 0.0f) { |
| 716 |
int line = (int)((ImGui::GetMousePos().x - origin.x) / bw); |
| 717 |
if (line < 0) |
| 718 |
line = 0; |
| 719 |
if (line >= (int)nlines) |
| 720 |
line = nlines - 1; |
| 721 |
const vera_bw_line_t &st = lines[line]; |
| 722 |
const uint32_t layer_clocks = |
| 723 |
((uint32_t)st.layer_fetches[0] + st.layer_fetches[1]) * VERA_BW_ACCESS_CLOCKS; |
| 724 |
const uint32_t port_clocks = (uint32_t)st.port_accesses * VERA_BW_ACCESS_CLOCKS; |
| 725 |
|
| 726 |
ImGui::BeginTooltip(); |
| 727 |
ImGui::Text("line %d", line); |
| 728 |
ImGui::Text("layer 0: %u fetches, layer 1: %u", st.layer_fetches[0], st.layer_fetches[1]); |
| 729 |
ImGui::Text("CPU port: %u access(es)", st.port_accesses); |
| 730 |
ImGui::Text("%u of %d clocks", layer_clocks + port_clocks, VERA_BW_LINE_CLOCKS); |
| 731 |
|
| 732 |
|
| 733 |
|
| 734 |
|
| 735 |
|
| 736 |
|
| 737 |
|
| 738 |
|
| 739 |
|
| 740 |
|
| 741 |
|
| 742 |
|
| 743 |
const uint32_t taken = layer_clocks + port_clocks; |
| 744 |
const uint32_t left = taken < VERA_BW_LINE_CLOCKS ? VERA_BW_LINE_CLOCKS - taken : 0; |
| 745 |
ImGui::Separator(); |
| 746 |
ImGui::Text("left for sprites: %u bus clocks", left); |
| 747 |
|
| 748 |
const sprite_trace_frame_t *sf = sprite_trace_last_frame(); |
| 749 |
if (sf && line < SPRITE_TRACE_LINES) { |
| 750 |
const sprite_line_stat_t &ss = sf->lines[line]; |
| 751 |
if (ss.demand) { |
| 752 |
|
| 753 |
|
| 754 |
|
| 755 |
|
| 756 |
uint32_t sprite_bus = 0; |
| 757 |
for (int slot = 0; slot < SPRITE_TRACE_SLOTS; ++slot) { |
| 758 |
const size_t k = (size_t)line * SPRITE_TRACE_SLOTS + slot; |
| 759 |
if (!(sf->line_flags[k] & SPRITE_TRACE_ONSCREEN)) |
| 760 |
continue; |
| 761 |
const uint16_t g = sf->line_gen[k]; |
| 762 |
if (g == SPRITE_TRACE_NO_GEN || g >= sf->gen_count) |
| 763 |
continue; |
| 764 |
const sprite_gen_t &gen = sf->gens[g]; |
| 765 |
sprite_bus += vera_bandwidth_sprite_bus_clocks(gen.width, gen.color_mode); |
| 766 |
} |
| 767 |
ImGui::Text("sprites: %u bus clocks (%u render clocks)", sprite_bus, ss.demand); |
| 768 |
if (sprite_bus > left) |
| 769 |
ImGui::TextColored(ImVec4(1, 0.75f, 0.30f, 1), |
| 770 |
"more bus than was free: sprites are last in\n" |
| 771 |
"priority, so they wait -- and the sprite model\n" |
| 772 |
"charges one clock a fetch where the bus takes\n" |
| 773 |
"two, so it flatters this line"); |
| 774 |
} |
| 775 |
} |
| 776 |
ImGui::EndTooltip(); |
| 777 |
} |
| 778 |
|
| 779 |
ImGui::Spacing(); |
| 780 |
ImGui::TextDisabled("Sprite render time has its own view: VERA panel \xe2\x86\x92 Multiplex " |
| 781 |
"\xe2\x86\x92 Render time."); |
| 782 |
if (ImGui::IsItemHovered()) { |
| 783 |
ImGui::SetTooltip( |
| 784 |
"Sprites are LAST on VERA's bus (vram_if.v:142-157), so what the\n" |
| 785 |
"layers and the CPU leave is what they get.\n\n" |
| 786 |
"Two different clocks, and they must not be subtracted from each\n" |
| 787 |
"other: everything here is BUS OCCUPANCY, while the Render time\n" |
| 788 |
"view measures WALL CLOCK -- render_time_r ticks even while the\n" |
| 789 |
"renderer paints with the bus idle, so it runs about five times\n" |
| 790 |
"higher at 4 bpp. The tooltip above converts before comparing."); |
| 791 |
} |
| 792 |
} |
| 793 |
|
| 794 |
void |
| 795 |
perf_panel_render(bool *p_open) |
| 796 |
{ |
| 797 |
|
| 798 |
|
| 799 |
|
| 800 |
static bool armed = false; |
| 801 |
DebugUiSettings &s = debug_ui_settings(); |
| 802 |
const bool want = (p_open ? *p_open : true) || s.perf_always_on; |
| 803 |
if (want != armed) { |
| 804 |
perf_budget_arm(PERF_OWNER_UI, want); |
| 805 |
vera_bandwidth_arm(VERA_BW_OWNER_UI, want); |
| 806 |
if (want) { |
| 807 |
perf_budget_set_capacity(s.perf_capacity); |
| 808 |
perf_budget_set_target_fps(s.perf_target_fps); |
| 809 |
perf_budget_set_idle_mode((perf_idle_mode_t)s.perf_idle_mode); |
| 810 |
} |
| 811 |
armed = want; |
| 812 |
} |
| 813 |
|
| 814 |
if (!ImGui::Begin("Performance", p_open)) { |
| 815 |
dbgui_window_end(); |
| 816 |
return; |
| 817 |
} |
| 818 |
dbgui_window_zoom("perf"); |
| 819 |
|
| 820 |
if (!perf_budget_is_enabled()) { |
| 821 |
ImGui::TextDisabled("Profiling is off."); |
| 822 |
dbgui_window_end(); |
| 823 |
return; |
| 824 |
} |
| 825 |
|
| 826 |
draw_budget_header(); |
| 827 |
ImGui::Separator(); |
| 828 |
draw_current_frame(); |
| 829 |
ImGui::Separator(); |
| 830 |
|
| 831 |
if (ImGui::CollapsingHeader("History", ImGuiTreeNodeFlags_DefaultOpen)) |
| 832 |
draw_graph(); |
| 833 |
if (ImGui::CollapsingHeader("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) |
| 834 |
draw_statistics(); |
| 835 |
if (ImGui::CollapsingHeader("Breakdown", ImGuiTreeNodeFlags_DefaultOpen)) |
| 836 |
draw_breakdown(); |
| 837 |
if (ImGui::CollapsingHeader("Bandwidth", ImGuiTreeNodeFlags_DefaultOpen)) |
| 838 |
draw_bandwidth(); |
| 839 |
if (ImGui::CollapsingHeader("Zones")) |
| 840 |
draw_zone_editor(); |
| 841 |
if (ImGui::CollapsingHeader("Settings")) |
| 842 |
draw_controls(); |
| 843 |
|
| 844 |
dbgui_window_end(); |
| 845 |
} |
| 846 |
|
| 847 |
DebugPanelRegistration s_reg("Performance", perf_panel_render, true); |
| 848 |
|
| 849 |
} |