OXYGENHUB
# 2980b7b6d7cd enhanced-x16-emulator / src / debug_ui / panels / perf_panel.cpp
31.5 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// Commander X16 Emulator — ImGui "Performance" panel.
2
//
3
// Answers the question a program author actually has: at 60 fps (or 30, or
4
// whatever they are targeting) how much of the frame is their code using, and
5
// how much room is left?
6
//
7
// The accounting is not done here. src/perf_budget.c owns the cycle budget and
8
// src/vera_bandwidth.c owns the VRAM bus figures, because the same numbers are
9
// served over DAP and neither surface should have its own idea of what a frame
10
// cost. This file is a reader and a control panel.
11
//
12
// CPU CYCLES AND VERA BANDWIDTH SHARE THIS PANEL on purpose. They are two
13
// halves of "what is eating my frame?", and the frames worth looking at are the
14
// ones where they disagree.
15
//
16
// PROFILING IS ARMED BY THE PANEL BEING OPEN, and disarmed by closing it. This
17
// panel ships open and docked with the other bottom-row views, so a normal
18
// `-imgui` session profiles from boot -- which is the point: the numbers are
19
// only useful if they are already there when you go looking, and a panel that
20
// starts empty and fills over the next minute answers nothing about the frame
21
// that made you open it.
22
//
23
// What that costs the running machine is an add, one array index and a couple
24
// of branches per instruction, in the same class as the
25
// code_map_record_current() call `-imgui` already makes on every instruction.
26
// Close the tab and it stops; System > Settings has the reverse (keep profiling
27
// with the panel shut, for feeding a DAP client).
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
// The windows the statistics table reports, matching what the DAP surface
42
// offers so that the panel and a client never disagree about what "30s" means.
43
struct Window {
44
	const char *name;
45
	float       seconds; // <= 0 means the whole ring
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
// Graph history, refilled each frame from the ring.
53
float  g_graph[1024];
54
int    g_graph_n = 0;
55
56
// Zone editor scratch.
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); // over budget
69
	if (pct >= 90.0f)
70
		return ImVec4(1.00f, 0.75f, 0.30f, 1.0f); // no headroom left
71
	return ImVec4(0.45f, 0.90f, 0.50f, 1.0f);
72
}
73
74
// Cycles as a share of a scanline, which is the unit X16 programs are written
75
// in far more often than raw cycles.
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
	// A stacked bar: work (of which interrupt) against idle, drawn to the width
166
	// of the BUDGET rather than the frame, so overrunning visibly runs past the
167
	// end instead of quietly rescaling.
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
	// Idle first, as the backdrop the work sits on.
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
	// The budget line, so "how close am I" is readable at a glance.
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
	// perf_budget_frames() would be a second copy of the ring; the panel only
248
	// needs the work figure, so it walks the window API instead.
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
	// Oldest to newest, so the graph reads left-to-right like a timeline.
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
	// The budget line over the top: without it the histogram says how much the
282
	// work varies but not whether any of it mattered.
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
	// Picking a zone straight off a symbol is the reason to bother with a zone
473
	// editor at all: nobody knows the address range of sprite_update by heart,
474
	// and the .dbg already does.
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
					// A symbol has no recorded size, so the zone runs to the
497
					// next symbol above it. Honest, and usually right for a
498
					// routine; the fields stay editable either way.
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
// Bandwidth: VERA's side of "what is eating my frame?".
582
//
583
// The CPU budget above and this share one panel deliberately. The interesting
584
// frames are the ones where the two disagree -- code with cycles to spare that
585
// still cannot get its pixels through the bus, or a bus with room left over
586
// while the CPU drowns.
587
//
588
// The unit is CLOCKS OF ONE SCANLINE, not a percentage of VERA's total
589
// bandwidth. A frame uses maybe a fifth of the chip's 100 MB/s, so a
590
// percent-of-peak reading would sit near 20% forever and tell nobody anything
591
// -- the same trap as reporting raw cycles-per-frame, which is always 100%.
592
// The scanline is the window that genuinely runs out.
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
	// ── Layers ──────────────────────────────────────────────────────────────
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
	// ── The data port, which is the part a program controls directly ────────
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
	// ── Per-scanline bars, drawn like the sprite render-time view ───────────
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
	// Same scaling rule as the sprite view: track the peak, but never squash
690
	// the ceiling below three-quarters height, so an ordinary frame does not
691
	// look alarmingly full.
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
		// What was left for the sprite renderer, which is last on the bus
733
		// (vram_if.v:142-157). Crossed with the sprite trace HERE rather than
734
		// inside vera_bandwidth.c, which is what keeps that module free of any
735
		// dependency on this one.
736
		//
737
		// The sprite figures have to be CONVERTED first. sprite_trace measures
738
		// render time -- render_time_r ticks every clock, including the ones
739
		// painting the line buffer with the bus idle -- while everything here
740
		// is bus occupancy. Comparing the two directly overstates sprite
741
		// pressure about fivefold at 4 bpp and would warn about lines that
742
		// comfortably fit.
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
				// Sum the fetches of the sprites that were actually on this
753
				// line. A slot cut short by the render-time ceiling fetched
754
				// less than its full width, so this is an upper bound on
755
				// exhausted lines -- which the sprite view already flags.
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
	// Arm profiling for as long as the panel is open. Done before Begin() so a
798
	// collapsed-but-open panel keeps collecting: the user has said they are
799
	// interested, and a gap in the history helps nobody.
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
} // namespace