OXYGENHUB
◆ main openmsx / src / imgui / ImGuiTraceViewer.cc
70.1 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "ImGuiTraceViewer.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiOpenFile.hh"
5
#include "ImGuiUtils.hh"
6
#include "Shortcuts.hh"
7
8
#include "Debugger.hh"
9
#include "EmuDuration.hh"
10
#include "MSXMotherBoard.hh"
11
#include "ReverseManager.hh"
12
13
#include "find_closest.h"
14
#include "one_of.hh"
15
#include "timeline.hh"
16
17
#include "imgui_stdlib.h"
18
19
#include <algorithm>
20
#include <cassert>
21
22
namespace openmsx {
23
24
using Traces = std::span<Tracer::Trace*>;
25
using Events = std::span<const Tracer::Event>;
26
27
constexpr float splitterWidth = 7.0f; // should be odd
28
29
struct Convertor {
30
	EmuTime viewStartTime;
31
	float viewScreenWidth;
32
	float pixelsPerTick;
33
34
	Convertor(EmuTime viewStartTime_, EmuDuration viewDuration, float viewScreenWidth_)
35
		: viewStartTime(viewStartTime_)
36
		, viewScreenWidth(viewScreenWidth_)
37
		, pixelsPerTick(viewScreenWidth_ / float(viewDuration.toUint64()))
38
	{}
39
40
	[[nodiscard]] float timeToX(EmuTime time) const {
41
		int64_t dt = int64_t(time.toUint64()) - int64_t(viewStartTime.toUint64());
42
		return float(dt) * pixelsPerTick;
43
	}
44
45
	[[nodiscard]] EmuDuration deltaXtoDuration(float deltaX) const {
46
		return EmuDuration(uint64_t(deltaX / pixelsPerTick));
47
	}
48
	[[nodiscard]] EmuTime xToTime(float x) const {
49
		return viewStartTime + deltaXtoDuration(x);
50
	}
51
};
52
53
static auto calcRulerTicks(double begin, double end, double n)
54
{
55
	struct Result {
56
		double first;
57
		double step;
58
	};
59
60
	auto length = end - begin;
61
	auto i = length / n;
62
	auto magnitude = std::pow(10.0, std::floor(std::log10(i)));
63
	auto mantissa = i / magnitude;
64
	auto snap = (mantissa >= 5.0) ? 10.0
65
	          : (mantissa >= 2.0) ?  5.0
66
	                              :  2.0;
67
	auto step = snap * magnitude;
68
	auto first = std::floor(begin / step) * step;
69
	return Result{.first = first, .step = step};
70
}
71
72
// Generate timeline labels with consistent formatting (fixed or
73
// scientific notation) for the range [start, stop).
74
//
75
// Parameters:
76
//   from, to:   Range to generate labels for
77
//   numLabels:  Preferred number of labels (upper bound)
78
//
79
// Returns:
80
//  * Vector of Labels at positions start, start+step, start+2*step, ...
81
//    (Cached - repeated calls with same parameters return immediately)
82
//  * step size
83
std::pair<const std::vector<TimelineFormatter::Label>&, double>
84
TimelineFormatter::calc(double from, double to, double numLabels, double unitFactor)
85
{
86
	// Check if parameters changed - avoid recalculation if same
87
	Params params{.from = from, .to = to, .numLabels = numLabels, .unitFactor = unitFactor};
88
	if (params == prevParams) return {labels, step}; // parameters unchanged, return cache
89
	prevParams = params;
90
91
	auto ruler = calcRulerTicks(from * unitFactor, to * unitFactor, numLabels);
92
	auto start = ruler.first;
93
	step = ruler.step;
94
	auto stop = start + step * std::ceil(numLabels);
95
96
	// Use a representative value for format decision (avoid zero
97
	// since it's atypical) Given start < stop requirement,
98
	// testValue is always non-zero
99
	auto testValue = (start == 0.0) ? stop : start;
100
	auto absTestValue = std::abs(testValue);
101
102
	// Calculate common subexpressions
103
	auto logStep = std::log10(step);
104
	int ceilNegLogStep = -static_cast<int>(std::floor(
105
		logStep)); // ceil(-logStep) = -floor(logStep)
106
	int e = static_cast<int>(std::floor(std::log10(absTestValue)));
107
108
	// Calculate scientific notation parameters
109
	// Since step = {1, 2, 5} × 10^n, we can derive precision from
110
	// the step size and the value's exponent: precision =
111
	// ceil(exponent - log10(step)) Using identity: ceil(e -
112
	// logStep) = e + ceil(-logStep) for integer e
113
	int sciPrecision = e + ceilNegLogStep;
114
	int sciLen = sciPrecision + 5; // Format: d.dddde±ee = 1 + prec
115
					// + 'e' + sign + 2 digits
116
117
	// Calculate fixed notation parameters
118
	int decimals = (step < 1.0) ? ceilNegLogStep : 0;
119
	int intDigits =
120
		(absTestValue < 1.0)
121
			? 1
122
			: e + 1; // e+1 converts exponent to digit count
123
	int fixedLen = intDigits + decimals;
124
125
	// Decide format by comparing approximate string lengths
126
	// Assumptions: ignore sign (equal in both), ignore decimal
127
	// point (narrow), assume exponent fits in 2 digits (±00 to ±99,
128
	// reasonable for timelines)
129
	bool useScientific = sciLen < fixedLen;
130
131
	// Generate all labels in the chosen format
132
	// Note: Using snprintf for now. Future: switch to std::format
133
	// (C++20, GCC 13+)
134
	labels.clear();
135
	auto value = start;
136
	char buf[32];
137
	if (useScientific) {
138
		while (value < stop) {
139
			int len = snprintf(buf, sizeof(buf), "%.*e",
140
						sciPrecision, value);
141
			labels.push_back(
142
				{value / unitFactor, std::string(buf, len)});
143
			value += step;
144
		}
145
	} else {
146
		while (value < stop) {
147
			int len = snprintf(buf, sizeof(buf), "%.*f",
148
						decimals, value);
149
			labels.push_back(
150
				{value / unitFactor, std::string(buf, len)});
151
			value += step;
152
		}
153
	}
154
	return {labels, step};
155
}
156
157
ImGuiTraceViewer::ImGuiTraceViewer(ImGuiManager& manager_)
158
	: ImGuiPart(manager_)
159
{
160
}
161
162
void ImGuiTraceViewer::save(ImGuiTextBuffer& buf)
163
{
164
	savePersistent(buf, *this, persistentElements);
165
}
166
167
void ImGuiTraceViewer::loadLine(std::string_view name, zstring_view value)
168
{
169
	loadOnePersistent(name, value, *this, persistentElements);
170
}
171
172
static void drawText(ImDrawList* drawList, gl::vec2 pos, ImU32 color, std::string_view text)
173
{
174
	drawList->AddText(pos, color, text.data(), text.data() + text.size());
175
}
176
177
struct DrawCoarse {
178
	const Convertor* convertor;
179
	ImDrawList* drawList;
180
	float x;
181
	float y0, y1;
182
	ImU32 colorNormal;
183
	ImU32 colorHover;
184
	Tracer::Trace::Format format;
185
	bool rowHovered;
186
187
	void operator()(float x0, float x1, auto first, auto last) const {
188
		auto xf0 = std::floor(x0 + x);
189
		auto xf1 = std::floor(x1 + x + 1.0f);
190
		drawList->AddRectFilled({xf0, y0}, {xf1, y1}, colorNormal);
191
192
		if (!rowHovered) return;
193
		auto mouseX = ImGui::GetIO().MousePos.x;
194
		if (mouseX < xf0 || xf1 <= mouseX) return;
195
		drawList->AddLine({mouseX, y0}, {mouseX, y1}, colorHover, 2.0f);
196
197
		if (!convertor) return;
198
		auto [it, _] = find_closest(
199
			first, last, convertor->xToTime(mouseX), {}, &Tracer::Event::time);
200
		if (it == last) return;
201
		if (it->value.template holds_alternative<std::monostate>()) {
202
			++it; // 2nd try
203
			if (it == last) return;
204
		}
205
		std::array<char, 64> tmpBuf;
206
		auto valueStr = ImGuiTraceViewer::formatTraceValue(it->value, tmpBuf, format);
207
		im::Tooltip([&]{
208
			ImGui::TextUnformatted(valueStr);
209
		});
210
	}
211
};
212
213
static void drawEventsVoid(
214
	gl::vec2 topLeft, const Convertor& convertor, EmuTime maxT, Events events, bool rowHovered)
215
{
216
	auto* drawList = ImGui::GetWindowDrawList();
217
	auto colorLine = ImGui::GetColorU32(ImGuiCol_PlotLines);
218
	auto h = ImGui::GetFrameHeight();
219
	auto h5 = h * 0.20f;
220
	auto y0 = topLeft.y;
221
	auto y1 = y0 + h;
222
	auto y5 = y0 + h5;
223
224
	auto minX = drawList->GetClipRectMin().x;
225
	auto maxX = drawList->GetClipRectMax().x;
226
	drawList->AddLine({minX, y1}, {maxX, y1}, colorLine);
227
	if (events.empty()) return;
228
229
	auto highLightTime = [&] {
230
		if (!rowHovered) return EmuTime::infinity(); // no highlight
231
		const auto& io = ImGui::GetIO();
232
		auto mouseX = io.MousePos.x - topLeft.x; // includes scroll offset
233
		auto [it, dist] = find_closest(
234
			events, convertor.xToTime(mouseX), {}, &Tracer::Event::time);
235
		assert(it != events.end());
236
		return (dist < convertor.deltaXtoDuration(h5)) ? it->time : EmuTime::infinity();
237
	}();
238
239
	auto colorNormal = ImGui::GetColorU32(ImGuiCol_PlotHistogram);
240
	auto colorHover = ImGui::GetColorU32(ImGuiCol_PlotHistogramHovered);
241
242
	auto drawDetailed = [&](float x0, float /*x1*/, const Tracer::Event& event) {
243
		x0 += topLeft.x;
244
		std::array<ImVec2, 3> points = {
245
			gl::vec2{x0 - h5, y5},
246
			gl::vec2{x0     , y0},
247
			gl::vec2{x0 + h5, y5},
248
		};
249
		bool hover = event.time == highLightTime;
250
		auto color = hover ? colorHover : colorNormal;
251
		auto thickness = hover ? 2.0f : 1.0f;
252
		drawList->AddPolyline(points.data(), narrow<int>(points.size()), color, thickness);
253
		drawList->AddLine({x0, y0}, {x0, y1}, color, thickness);
254
	};
255
	auto dummyFormat = Tracer::Trace::Format::DEC;
256
	processTimeline(events, maxT, convertor, drawDetailed,
257
	                DrawCoarse{nullptr, drawList, topLeft.x, y0, y1, colorNormal, colorHover, dummyFormat, rowHovered});
258
}
259
260
static void drawEventsBool(
261
	gl::vec2 topLeft, const Convertor& convertor, EmuTime maxT, Events events, bool rowHovered)
262
{
263
	if (events.empty()) return;
264
265
	auto* drawList = ImGui::GetWindowDrawList();
266
	auto colorNormal = ImGui::GetColorU32(ImGuiCol_PlotLines);
267
	auto colorHover = ImGui::GetColorU32(ImGuiCol_PlotLinesHovered);
268
	auto h = ImGui::GetFrameHeight();
269
	auto y0 = topLeft.y;
270
	auto y1 = y0 + h;
271
272
	const auto& io = ImGui::GetIO();
273
	auto mouseX = io.MousePos.x;
274
275
	auto getVal = [&](const Tracer::Event& e) {
276
		return e.value.get_as_bool() ? y0 : y1;
277
	};
278
279
	auto yp = y1;
280
	auto drawDetailed = [&](float x0, float x1, const Tracer::Event& event) {
281
		auto xf0 = topLeft.x + x0;
282
		auto xf1 = topLeft.x + x1;
283
		auto y = getVal(event);
284
285
		std::array<ImVec2, 3> points = {
286
			gl::vec2{xf1, y},
287
			gl::vec2{xf0, y},
288
			gl::vec2{xf0, yp},
289
		};
290
		auto hover = rowHovered && xf0 <= mouseX && mouseX < xf1;
291
		auto color = hover ? colorHover : colorNormal;
292
		auto thickness = hover ? 2.0f : 1.0f;
293
		drawList->AddPolyline(points.data(), (yp == y) ? 2 : 3, color, thickness);
294
295
		yp = y;
296
	};
297
	auto dummyFormat = Tracer::Trace::Format::DEC;
298
	processTimeline(events, maxT, convertor, drawDetailed,
299
	                DrawCoarse{nullptr, drawList, topLeft.x, y0, y1, colorNormal, colorHover, dummyFormat, rowHovered});
300
}
301
302
[[nodiscard]] std::string_view ImGuiTraceViewer::formatTraceValue(const TraceValue& value, std::span<char, 64> tmpBuf,
303
                                                                  Tracer::Trace::Format format)
304
{
305
	return value.visit(overloaded{
306
		[](std::monostate) {
307
			return std::string_view{};
308
		},
309
		[&](uint64_t i) {
310
			switch (format) {
311
			using enum Tracer::Trace::Format;
312
			case BIN: {
313
				auto bin = bin_string(i);
314
				auto* begin = tmpBuf.data();
315
				auto* end = bin.copy(begin);
316
				return std::string_view(begin, end);
317
			}
318
			default:
319
			case DEC: {
320
				auto [end, ec] = std::to_chars(tmpBuf.data(), tmpBuf.data() + tmpBuf.size(), i);
321
				assert(ec == std::errc{}); // should never fail (for sufficiently large buffer)
322
				return std::string_view(tmpBuf.data(), end - tmpBuf.data());
323
			}
324
			case HEX: {
325
				auto hex = hex_string(i);
326
				auto* begin = tmpBuf.data();
327
				auto* end = hex.copy(begin);
328
				return std::string_view(begin, end);
329
			}
330
			}
331
		},
332
		[&](double d) {
333
		#if 0
334
			// doesn't work yet in MacOS :(
335
			auto [end, ec] = std::to_chars(tmpBuf.data(), tmpBuf.data() + tmpBuf.size(),
336
			                               d, std::chars_format::general);
337
			assert(ec == std::errc{}); // should never fail (for sufficiently large buffer)
338
			return std::string_view(tmpBuf.data(), end - tmpBuf.data());
339
		#endif
340
			int len = snprintf(tmpBuf.data(), tmpBuf.size(), "%.17g", d);
341
			assert(len > 0 && size_t(len) < tmpBuf.size());
342
			return std::string_view(tmpBuf.data(), len);
343
		},
344
		[](std::string_view s) {
345
			return s;
346
		}
347
	});
348
}
349
350
static void drawEventsValue(
351
	gl::vec2 topLeft, const Convertor& convertor, EmuTime maxT, Events events, bool rowHovered,
352
	Tracer::Trace::Format format)
353
{
354
	auto* drawList = ImGui::GetWindowDrawList();
355
	const auto& style = ImGui::GetStyle();
356
	auto colorNormal = ImGui::GetColorU32(ImGuiCol_PlotLines);
357
	auto colorHover = ImGui::GetColorU32(ImGuiCol_PlotLinesHovered);
358
	auto colorText = getColor(imColor::TEXT);
359
	auto h = ImGui::GetFrameHeight();
360
	auto h2 = h * 0.50f;
361
	auto h5 = h * 0.20f;
362
	auto y0 = topLeft.y;
363
	auto y1 = y0 + h;
364
	auto y2 = y0 + h2;
365
366
	const auto& io = ImGui::GetIO();
367
	auto mouseX = io.MousePos.x;
368
369
	auto drawDetailed = [&](float x0, float x1, const Tracer::Event& event) {
370
		auto xf0 = topLeft.x + x0;
371
		auto xf1 = topLeft.x + x1;
372
		auto hover = rowHovered && xf0 <= mouseX && mouseX < xf1;
373
		auto color = hover ? colorHover : colorNormal;
374
		auto thickness = hover ? 2.0f : 1.0f;
375
376
		auto tl = gl::vec2{xf0, y0};
377
		auto br = gl::vec2{xf1, y1};
378
		if (event.value.holds_alternative<std::monostate>()) {
379
			drawList->AddLine({xf0, y2}, {xf1, y2}, color, thickness);
380
		} else {
381
			std::array<char, 64> tmpBuf;
382
			auto valueStr = ImGuiTraceViewer::formatTraceValue(event.value, tmpBuf, format);
383
			if (auto maxWidth = x1 - x0 - 2.0f * h5; maxWidth > 0.0f) {
384
				std::array<ImVec2, 6> points = {
385
					gl::vec2{xf1 - h5, y0},
386
					gl::vec2{xf0 + h5, y0},
387
					gl::vec2{xf0,      y2},
388
					gl::vec2{xf0 + h5, y1},
389
					gl::vec2{xf1 - h5, y1},
390
					gl::vec2{xf1,      y2},
391
				};
392
				drawList->AddPolyline(points.data(), 6, color, thickness, ImDrawFlags_Closed);
393
				drawList->PushClipRect(tl, br, true);
394
				auto textX = std::max(xf0 + h5, topLeft.x);
395
				drawText(drawList, gl::vec2{textX, y0 + style.FramePadding.y}, colorText, valueStr);
396
				drawList->PopClipRect();
397
			} else {
398
				drawList->AddRect(tl, br, color, {}, thickness);
399
			}
400
			if (hover) {
401
				im::Tooltip([&]{
402
					ImGui::TextUnformatted(valueStr);
403
				});
404
			}
405
		}
406
	};
407
	processTimeline(events, maxT, convertor, drawDetailed,
408
	                DrawCoarse{&convertor, drawList, topLeft.x, y0, y1, colorNormal, colorHover, format, rowHovered});
409
}
410
411
static bool menuItemWithShortcut(ImGuiManager& manager, const char* label, Shortcuts::ID id, bool* p_selected = nullptr)
412
{
413
	const auto& shortcuts = manager.getShortcuts();
414
	const auto& shortcut = shortcuts.getShortcut(id);
415
	if (shortcut.keyChord == ImGuiKey_None) {
416
		return ImGui::MenuItem(label, nullptr, p_selected);
417
	} else {
418
		return ImGui::MenuItem(label, getKeyChordName(shortcut.keyChord).c_str(), p_selected);
419
	}
420
}
421
422
void ImGuiTraceViewer::drawMenuBar(EmuTime minT, EmuDuration totalT, Tracer& tracer)
423
{
424
	im::Menu("File", [&]{
425
		ImGui::MenuItem("Select probes and traces", nullptr, &showSelect);
426
		if (ImGui::MenuItem("Export as VCD ...")) {
427
			manager.openFile->selectNewFile(
428
				"Export to VCD", "VCD (*.vcd){.vcd}",
429
				[&](const auto& fn) {
430
					try {
431
						tracer.exportVCD(fn);
432
					} catch (MSXException& e) {
433
						manager.printError(
434
							"Couldn't export VCD: ", e.getMessage());
435
					}
436
				});
437
		}
438
		if (ImGui::MenuItem("Close")) show = false;
439
	});
440
	im::Menu("View", [&]{
441
		if (menuItemWithShortcut(manager, "Zoom to fit", Shortcuts::ID::TRACE_ZOOM_TO_FIT)) {
442
			zoomToFit(minT, totalT);
443
		}
444
		if (menuItemWithShortcut(manager, "Zoom in", Shortcuts::ID::TRACE_ZOOM_IN)) {
445
			doZoom(1.0 / 1.2, 0.5);
446
		}
447
		if (menuItemWithShortcut(manager, "Zoom out", Shortcuts::ID::TRACE_ZOOM_OUT)) {
448
			doZoom(1.2, 0.5);
449
		}
450
		ImGui::Separator();
451
452
		ImGui::TextUnformatted("Timeline:");
453
		im::Indent([&]{
454
			im::Menu("origin", [&]{
455
				ImGui::RadioButton("MSX boot", &timelineOrigin, Origin::ZERO);
456
				simpleToolTip("Absolute time since MSX boot");
457
				ImGui::RadioButton("Primary marker", &timelineOrigin, Origin::PRIMARY);
458
				simpleToolTip("Relative to primary marker");
459
				ImGui::RadioButton("Secondary marker", &timelineOrigin, Origin::SECONDARY);
460
				simpleToolTip("Relative to secondary marker");
461
				ImGui::RadioButton("Current time", &timelineOrigin, Origin::NOW);
462
				simpleToolTip("Relative to current emulation time");
463
			});
464
			im::Menu("start", [&]{
465
				if (ImGui::RadioButton("MSX boot", !timelineStart)) timelineStart = false;
466
				if (ImGui::RadioButton("First event", timelineStart)) timelineStart = true;
467
			});
468
			im::Menu("stop", [&]{
469
				if (ImGui::RadioButton("Current emulation time", !timelineStop)) timelineStop = false;
470
				if (ImGui::RadioButton("Last event", timelineStop)) timelineStop = true;
471
			});
472
			ImGui::MenuItem("Track current time", nullptr, &followNow);
473
			simpleToolTip("Auto-scroll so that the current emulation time remains visible (only if it was already visible)");
474
		});
475
		ImGui::Separator();
476
477
		menuItemWithShortcut(manager, "Show menu bar", Shortcuts::ID::TRACE_SHOW_MENU_BAR, &showMenuBar);
478
	});
479
	im::Menu("Goto", [&]{
480
		constexpr auto pos_text = "positive edge = rising edge = transition from low to high";
481
		constexpr auto neg_text = "negative edge = falling edge = transition from high to low";
482
		ImGui::TextUnformatted("Move primary marker to");
483
		simpleToolTip("Place via left-mouse-click on the graph.");
484
		im::Indent([&]{
485
			if (menuItemWithShortcut(manager, "Next edge", Shortcuts::ID::TRACE_NEXT_EDGE)) {
486
				gotoNextEdge(selectedTime1);
487
			}
488
			if (menuItemWithShortcut(manager, "Next positive edge", Shortcuts::ID::TRACE_NEXT_POS_EDGE)) {
489
				gotoNextPosEdge(selectedTime1);
490
			}
491
			simpleToolTip(pos_text);
492
			if (menuItemWithShortcut(manager, "Next negative edge", Shortcuts::ID::TRACE_NEXT_NEG_EDGE)) {
493
				gotoNextNegEdge(selectedTime1);
494
			}
495
			simpleToolTip(neg_text);
496
			if (menuItemWithShortcut(manager, "Previous edge", Shortcuts::ID::TRACE_PREV_EDGE)) {
497
				gotoPrevEdge(selectedTime1);
498
			}
499
			if (menuItemWithShortcut(manager, "Previous positive edge", Shortcuts::ID::TRACE_PREV_POS_EDGE)) {
500
				gotoPrevPosEdge(selectedTime1);
501
			}
502
			simpleToolTip(pos_text);
503
			if (menuItemWithShortcut(manager, "Previous negative edge", Shortcuts::ID::TRACE_PREV_NEG_EDGE)) {
504
				gotoPrevNegEdge(selectedTime1);
505
			}
506
			simpleToolTip(neg_text);
507
		});
508
		ImGui::Separator();
509
510
		ImGui::TextUnformatted("Move secondary marker to");
511
		simpleToolTip("Place via shift-left-mouse-click on the graph.");
512
		im::Indent([&]{
513
			if (menuItemWithShortcut(manager, "Next edge##alt", Shortcuts::ID::TRACE_ALT_NEXT_EDGE)) {
514
				gotoNextEdge(selectedTime2);
515
			}
516
			if (menuItemWithShortcut(manager, "Next positive edge##alt", Shortcuts::ID::TRACE_ALT_NEXT_POS_EDGE)) {
517
				gotoNextPosEdge(selectedTime2);
518
			}
519
			simpleToolTip(pos_text);
520
			if (menuItemWithShortcut(manager, "Next negative edge##alt", Shortcuts::ID::TRACE_ALT_NEXT_NEG_EDGE)) {
521
				gotoNextNegEdge(selectedTime2);
522
			}
523
			simpleToolTip(neg_text);
524
			if (menuItemWithShortcut(manager, "Previous edge##alt", Shortcuts::ID::TRACE_ALT_PREV_EDGE)) {
525
				gotoPrevEdge(selectedTime2);
526
			}
527
			if (menuItemWithShortcut(manager, "Previous positive edge##alt", Shortcuts::ID::TRACE_ALT_PREV_POS_EDGE)) {
528
				gotoPrevPosEdge(selectedTime2);
529
			}
530
			simpleToolTip(pos_text);
531
			if (menuItemWithShortcut(manager, "Previous negative edge##alt", Shortcuts::ID::TRACE_ALT_PREV_NEG_EDGE)) {
532
				gotoPrevNegEdge(selectedTime2);
533
			}
534
			simpleToolTip(neg_text);
535
		});
536
	});
537
	im::Menu("Help", [&]{
538
		if (ImGui::MenuItem("Overview")) {
539
			showHelp = true;
540
			helpSection = HelpSection::OVERVIEW;
541
		}
542
		if (ImGui::MenuItem("Probes and Traces")) {
543
			showHelp = true;
544
			helpSection = HelpSection::PROBES_AND_TRACES;
545
		}
546
		if (ImGui::MenuItem("Graphs")) {
547
			showHelp = true;
548
			helpSection = HelpSection::GRAPHS;
549
		}
550
		if (ImGui::MenuItem("Navigation")) {
551
			showHelp = true;
552
			helpSection = HelpSection::NAVIGATION;
553
		}
554
		if (ImGui::MenuItem("Example")) {
555
			showHelp = true;
556
			helpSection = HelpSection::EXAMPLE;
557
		}
558
	});
559
}
560
561
template<size_t N>
562
static void thinWhiteLine(ImDrawList* drawList, const std::array<ImVec2, N>& points)
563
	requires(N == 2)
564
{
565
	auto color = getColor(imColor::TEXT);
566
	drawList->AddLine(points[0], points[1], color, 1.0f);
567
	drawList->AddPolyline(points.data(), narrow<int>(points.size()), color, 1.0f);
568
}
569
template<size_t N>
570
static void thinWhiteLine(ImDrawList* drawList, const std::array<ImVec2, N>& points)
571
	requires(N != 2)
572
{
573
	auto color = getColor(imColor::TEXT);
574
	drawList->AddPolyline(points.data(), narrow<int>(points.size()), color, 1.0f);
575
}
576
static void arrowLeft(ImDrawList* drawList, gl::vec2 center, float u)
577
{
578
	std::array<ImVec2, 3> line1 = {
579
		center + u * gl::vec2{ 4.0f, -3.0f},
580
		center + u * gl::vec2{ 1.0f,  0.0f},
581
		center + u * gl::vec2{ 4.0f,  3.0f},
582
	};
583
	thinWhiteLine(drawList, line1);
584
	std::array<ImVec2, 2> line2 = {
585
		center + u * gl::vec2{ 1.0f, 0.0f},
586
		center + u * gl::vec2{10.0f, 0.0f},
587
	};
588
	thinWhiteLine(drawList, line2);
589
}
590
static void arrowRight(ImDrawList* drawList, gl::vec2 center, float u)
591
{
592
	std::array<ImVec2, 3> line1 = {
593
		center + u * gl::vec2{-4.0f, -3.0f},
594
		center + u * gl::vec2{-1.0f,  0.0f},
595
		center + u * gl::vec2{-4.0f,  3.0f},
596
	};
597
	thinWhiteLine(drawList, line1);
598
	std::array<ImVec2, 2> line2 = {
599
		center + u * gl::vec2{- 1.0f, 0.0f},
600
		center + u * gl::vec2{-10.0f, 0.0f},
601
	};
602
	thinWhiteLine(drawList, line2);
603
}
604
static void magnifyingGlass(ImDrawList* drawList, gl::vec2 center, float u)
605
{
606
	auto color = getColor(imColor::TEXT);
607
	drawList->AddCircle(center + u * gl::vec2{-1.0f, -1.0f}, 8.0f, color);
608
	std::array<ImVec2, 2> line = {
609
		center + u * gl::vec2{ 4.6f, 4.6f},
610
		center + u * gl::vec2{ 9.0f, 9.0f},
611
	};
612
	thinWhiteLine(drawList, line);
613
}
614
615
static void RenderZoomFit(gl::vec2 center, ImDrawList* drawList)
616
{
617
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
618
	magnifyingGlass(drawList, center, u);
619
	std::array<ImVec2, 2> line1 = {
620
		center + u * gl::vec2{-6.0f, -1.0f},
621
		center + u * gl::vec2{ 4.0f, -1.0f},
622
	};
623
	thinWhiteLine(drawList, line1);
624
	std::array<ImVec2, 3> line2 = {
625
		center + u * gl::vec2{-3.0f, -4.0f},
626
		center + u * gl::vec2{-6.0f, -1.0f},
627
		center + u * gl::vec2{-3.0f,  2.0f},
628
	};
629
	thinWhiteLine(drawList, line2);
630
	std::array<ImVec2, 3> line3 = {
631
		center + u * gl::vec2{ 1.0f, -4.0f},
632
		center + u * gl::vec2{ 4.0f, -1.0f},
633
		center + u * gl::vec2{ 1.0f,  2.0f},
634
	};
635
	thinWhiteLine(drawList, line3);
636
}
637
static void RenderZoomOut(gl::vec2 center, ImDrawList* drawList)
638
{
639
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
640
	magnifyingGlass(drawList, center, u);
641
	std::array<ImVec2, 2> line1 = {
642
		center + u * gl::vec2{-6.0f, -1.0f},
643
		center + u * gl::vec2{ 4.0f, -1.0f},
644
	};
645
	thinWhiteLine(drawList, line1);
646
}
647
static void RenderZoomIn(gl::vec2 center, ImDrawList* drawList)
648
{
649
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
650
	magnifyingGlass(drawList, center, u);
651
	std::array<ImVec2, 2> line1 = {
652
		center + u * gl::vec2{-6.0f, -1.0f},
653
		center + u * gl::vec2{ 4.0f, -1.0f},
654
	};
655
	thinWhiteLine(drawList, line1);
656
	std::array<ImVec2, 2> line2 = {
657
		center + u * gl::vec2{-1.0f, -6.0f},
658
		center + u * gl::vec2{-1.0f,  4.0f},
659
	};
660
	thinWhiteLine(drawList, line2);
661
}
662
static void RenderPrevNegEdge(gl::vec2 center, ImDrawList* drawList)
663
{
664
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
665
	std::array<ImVec2, 4> line1 = {
666
		center + u * gl::vec2{-9.0f, -9.0f},
667
		center + u * gl::vec2{-3.0f, -9.0f},
668
		center + u * gl::vec2{-3.0f,  9.0f},
669
		center + u * gl::vec2{ 3.0f,  9.0f},
670
	};
671
	thinWhiteLine(drawList, line1);
672
	arrowLeft(drawList, center, u);
673
}
674
static void RenderPrevPosEdge(gl::vec2 center, ImDrawList* drawList)
675
{
676
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
677
	std::array<ImVec2, 4> line1 = {
678
		center + u * gl::vec2{-9.0f,  9.0f},
679
		center + u * gl::vec2{-3.0f,  9.0f},
680
		center + u * gl::vec2{-3.0f, -9.0f},
681
		center + u * gl::vec2{ 3.0f, -9.0f},
682
	};
683
	thinWhiteLine(drawList, line1);
684
	arrowLeft(drawList, center, u);
685
}
686
static void RenderPrevEdge(gl::vec2 center, ImDrawList* drawList)
687
{
688
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
689
	std::array<ImVec2, 4> line1 = {
690
		center + u * gl::vec2{-9.0f,  9.0f},
691
		center + u * gl::vec2{-6.0f,  9.0f},
692
		center + u * gl::vec2{ 0.0f, -9.0f},
693
		center + u * gl::vec2{ 4.0f, -9.0f},
694
	};
695
	thinWhiteLine(drawList, line1);
696
	std::array<ImVec2, 4> line2 = {
697
		center + u * gl::vec2{-9.0f, -9.0f},
698
		center + u * gl::vec2{-6.0f, -9.0f},
699
		center + u * gl::vec2{ 0.0f,  9.0f},
700
		center + u * gl::vec2{ 4.0f,  9.0f},
701
	};
702
	thinWhiteLine(drawList, line2);
703
	arrowLeft(drawList, center, u);
704
}
705
static void RenderNextEdge(gl::vec2 center, ImDrawList* drawList)
706
{
707
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
708
	std::array<ImVec2, 4> line1 = {
709
		center + u * gl::vec2{ 9.0f,  9.0f},
710
		center + u * gl::vec2{ 6.0f,  9.0f},
711
		center + u * gl::vec2{ 0.0f, -9.0f},
712
		center + u * gl::vec2{-4.0f, -9.0f},
713
	};
714
	thinWhiteLine(drawList, line1);
715
	std::array<ImVec2, 4> line2 = {
716
		center + u * gl::vec2{ 9.0f, -9.0f},
717
		center + u * gl::vec2{ 6.0f, -9.0f},
718
		center + u * gl::vec2{ 0.0f,  9.0f},
719
		center + u * gl::vec2{-4.0f,  9.0f},
720
	};
721
	thinWhiteLine(drawList, line2);
722
	arrowRight(drawList, center, u);
723
}
724
static void RenderNextNegEdge(gl::vec2 center, ImDrawList* drawList)
725
{
726
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
727
	std::array<ImVec2, 4> line1 = {
728
		center + u * gl::vec2{ 9.0f,  9.0f},
729
		center + u * gl::vec2{ 3.0f,  9.0f},
730
		center + u * gl::vec2{ 3.0f, -9.0f},
731
		center + u * gl::vec2{-3.0f, -9.0f},
732
	};
733
	thinWhiteLine(drawList, line1);
734
	arrowRight(drawList, center, u);
735
}
736
static void RenderNextPosEdge(gl::vec2 center, ImDrawList* drawList)
737
{
738
	auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f);
739
	std::array<ImVec2, 4> line1 = {
740
		center + u * gl::vec2{ 9.0f, -9.0f},
741
		center + u * gl::vec2{ 3.0f, -9.0f},
742
		center + u * gl::vec2{ 3.0f,  9.0f},
743
		center + u * gl::vec2{-3.0f,  9.0f},
744
	};
745
	thinWhiteLine(drawList, line1);
746
	arrowRight(drawList, center, u);
747
}
748
749
void ImGuiTraceViewer::zoomToFit(EmuTime minT, EmuDuration totalT)
750
{
751
	viewStartTime = minT;
752
	viewDuration = totalT;
753
	scrollChanged = true;
754
}
755
756
void ImGuiTraceViewer::doZoom(double factor, double xFract)
757
{
758
	auto newDuration = std::max(viewDuration * factor, EmuDuration(100u));
759
	auto realFactor = newDuration.div(viewDuration);
760
761
	if (realFactor > 1.0) {
762
		auto offset = viewDuration * (xFract * (realFactor - 1.0));
763
		viewStartTime = viewStartTime.saturateSubtract(offset);
764
	} else {
765
		auto offset = viewDuration * (xFract * (1.0 - realFactor));
766
		viewStartTime += offset;
767
	}
768
769
	viewDuration = newDuration;
770
771
	scrollChanged = true;
772
}
773
774
void ImGuiTraceViewer::scrollTo(EmuTime time)
775
{
776
	if (time < viewStartTime) {
777
		viewStartTime = time.saturateSubtract(viewDuration * 0.2);
778
	} else if (time >= viewStartTime + viewDuration) {
779
		viewStartTime = time.saturateSubtract(viewDuration * 0.8);
780
	}
781
	scrollChanged = true;
782
}
783
784
[[nodiscard]] static Tracer::Trace* getTrace(Traces traces, int row)
785
{
786
	if (row < 0 || row >= int(traces.size())) return nullptr;
787
	return traces[row];
788
}
789
790
[[nodiscard]] double ImGuiTraceViewer::getUnitConversionFactor() const
791
{
792
	static_assert(NUM_UNITS == 6);
793
	switch (units) {
794
	default: // shouldn't happen
795
	case       SECONDS: return 1.0;
796
	case MILLI_SECONDS: return 1'000.0;
797
	case MICRO_SECONDS: return 1'000'000.0;
798
	case        CYCLES: return 3'579'545.0;
799
	case           PAL: return (6 * 3'579'545.0) / (1368 * 313);
800
	case          NTSC: return (6 * 3'579'545.0) / (1368 * 262);
801
	}
802
}
803
804
[[nodiscard]] static const Tracer::Event* findStrictlySmaller(const Tracer::Trace& trace, EmuTime time)
805
{
806
	auto it = std::ranges::lower_bound(trace.events, time, {}, &Tracer::Event::time);
807
	if (it == trace.events.begin()) return nullptr;
808
	return std::to_address(it - 1);
809
}
810
811
[[nodiscard]] static const Tracer::Event* findStrictlyBigger(const Tracer::Trace& trace, EmuTime time)
812
{
813
	auto it = std::ranges::upper_bound(trace.events, time, {}, &Tracer::Event::time);
814
	if (it == trace.events.end()) return nullptr;
815
	return std::to_address(it);
816
}
817
818
void ImGuiTraceViewer::gotoPrevNegEdge(EmuTime& selectedTime)
819
{
820
	if (const auto* trace = getTrace(traces, selectedRow)) {
821
		auto time = selectedTime;
822
		while (true) {
823
			const auto* event = findStrictlySmaller(*trace, time);
824
			if (!event) return;
825
			time = event->time;
826
			if (!trace->isBool() || event->value.get_u64() == 0) break;
827
		}
828
		selectedTime = time;
829
		scrollTo(time);
830
	}
831
}
832
833
void ImGuiTraceViewer::gotoPrevPosEdge(EmuTime& selectedTime)
834
{
835
	if (const auto* trace = getTrace(traces, selectedRow)) {
836
		auto time = selectedTime;
837
		while (true) {
838
			const auto* event = findStrictlySmaller(*trace, time);
839
			if (!event) return;
840
			time = event->time;
841
			if (!trace->isBool() || event->value.get_u64() != 0) break;
842
		}
843
		selectedTime = time;
844
		scrollTo(time);
845
	}
846
}
847
848
void ImGuiTraceViewer::gotoPrevEdge(EmuTime& selectedTime)
849
{
850
	if (const auto* trace = getTrace(traces, selectedRow)) {
851
		if (const auto* event = findStrictlySmaller(*trace, selectedTime)) {
852
			selectedTime = event->time;
853
			scrollTo(event->time);
854
		}
855
	}
856
}
857
858
void ImGuiTraceViewer::gotoNextEdge(EmuTime& selectedTime)
859
{
860
	if (const auto* trace = getTrace(traces, selectedRow)) {
861
		if (const auto* event = findStrictlyBigger(*trace, selectedTime)) {
862
			selectedTime = event->time;
863
			scrollTo(event->time);
864
		}
865
	}
866
}
867
868
void ImGuiTraceViewer::gotoNextNegEdge(EmuTime& selectedTime)
869
{
870
	if (const auto* trace = getTrace(traces, selectedRow)) {
871
		auto time = selectedTime;
872
		while (true) {
873
			const auto* event = findStrictlyBigger(*trace, time);
874
			if (!event) return;
875
			time = event->time;
876
			if (!trace->isBool() || event->value.get_u64() == 0) break;
877
		}
878
		selectedTime = time;
879
		scrollTo(time);
880
	}
881
}
882
883
void ImGuiTraceViewer::gotoNextPosEdge(EmuTime& selectedTime)
884
{
885
	if (const auto* trace = getTrace(traces, selectedRow)) {
886
		auto time = selectedTime;
887
		while (true) {
888
			const auto* event = findStrictlyBigger(*trace, time);
889
			if (!event) return;
890
			time = event->time;
891
			if (!trace->isBool() || event->value.get_u64() != 0) break;
892
		}
893
		selectedTime = time;
894
		scrollTo(time);
895
	}
896
}
897
898
static void toolTipWithShortcut(ImGuiManager& manager, const char* text, Shortcuts::ID id)
899
{
900
	simpleToolTip([&] -> std::string {
901
		const auto& shortcuts = manager.getShortcuts();
902
		const auto& shortcut = shortcuts.getShortcut(id);
903
		if (shortcut.keyChord == ImGuiKey_None) return std::string(text);
904
		return strCat(text, " (", getKeyChordName(shortcut.keyChord), ')');
905
	});
906
}
907
908
[[nodiscard]] static std::string formatDouble(double value)
909
{
910
	std::array<char, 32> buffer; // 24 should also be sufficient, small safety margin
911
#if 0
912
	// doesn't work yet in MacOS :(
913
	auto [end, ec] = std::to_chars(
914
		buffer.data(), buffer.data() + buffer.size(),
915
		value, std::chars_format::general, 11);
916
	assert(ec == std::errc{}); // should never fail if we pass a sufficiently large buffer
917
	return std::string(buffer.data(), end - buffer.data());
918
#endif
919
	int len = snprintf(buffer.data(), buffer.size(), "%.11g", value);
920
	assert(len > 0 && size_t(len) < buffer.size());
921
	return std::string(buffer.data(), len);
922
}
923
924
void ImGuiTraceViewer::drawToolBar(EmuTime minT, EmuDuration totalT, float viewScreenWidth)
925
{
926
	if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
927
		if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
928
			auto& io = ImGui::GetIO();
929
			if (io.MouseWheel != 0.0f) {
930
				auto mx = io.MousePos.x - (ImGui::GetCursorScreenPos().x + col0_width + splitterWidth); // mouse x inside graph window
931
				auto xFract = mx / viewScreenWidth;
932
				doZoom(std::pow(1.2, double(-io.MouseWheel)), double(xFract));
933
				io.MouseWheel = 0.0f; // consume event
934
			}
935
		}
936
	}
937
938
	using enum Shortcuts::ID;
939
	const auto& shortcuts = manager.getShortcuts();
940
	if (shortcuts.checkShortcut(TRACE_SHOW_MENU_BAR)) {
941
		showMenuBar = !showMenuBar;
942
	}
943
	const auto& style = ImGui::GetStyle();
944
	gl::vec2 size{ImGui::GetFrameHeightWithSpacing()};
945
	if (ButtonWithCustomRendering("##ZoomFit", size, false, RenderZoomFit) ||
946
	    shortcuts.checkShortcut(TRACE_ZOOM_TO_FIT)) {
947
		zoomToFit(minT, totalT);
948
	}
949
	toolTipWithShortcut(manager, "Zoom to fit", TRACE_ZOOM_TO_FIT);
950
	ImGui::SameLine();
951
952
	if (ButtonWithCustomRendering("##ZoomOut", size, false, RenderZoomOut) ||
953
	    shortcuts.checkShortcut(TRACE_ZOOM_OUT)) {
954
		doZoom(1.2, 0.5);
955
	}
956
	toolTipWithShortcut(manager, "Zoom out", TRACE_ZOOM_OUT);
957
	ImGui::SameLine();
958
959
	if (ButtonWithCustomRendering("##ZoomIn", size, false, RenderZoomIn) ||
960
	    shortcuts.checkShortcut(TRACE_ZOOM_IN)) {
961
		doZoom(1.0 / 1.2, 0.5);
962
	}
963
	toolTipWithShortcut(manager, "Zoom in", TRACE_ZOOM_IN);
964
	ImGui::SameLine(0.0f, style.ItemSpacing.x * 2.0f);
965
966
	bool shift = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
967
	auto gotoStuff = [&](const char* label, const char* tooltip,
968
	                     Shortcuts::ID ID, Shortcuts::ID altID,
969
	                     auto renderFunc, auto action) {
970
		if (ButtonWithCustomRendering(label, size, false, renderFunc)) {
971
			std::invoke(action, this, shift ? selectedTime2 : selectedTime1);
972
		}
973
		toolTipWithShortcut(manager, tooltip, shift ? altID : ID);
974
		if (shortcuts.checkShortcut(ID)) {
975
			std::invoke(action, this, selectedTime1);
976
		}
977
		if (shortcuts.checkShortcut(altID)) {
978
			std::invoke(action, this, selectedTime2);
979
		}
980
	};
981
	gotoStuff("##PrevNegEdge", "Goto previous negative edge",
982
		TRACE_PREV_NEG_EDGE, TRACE_ALT_PREV_NEG_EDGE,
983
		RenderPrevNegEdge, &ImGuiTraceViewer::gotoPrevNegEdge);
984
	ImGui::SameLine();
985
	gotoStuff("##PrevPosEdge", "Goto previous positive edge",
986
		TRACE_PREV_POS_EDGE, TRACE_ALT_PREV_POS_EDGE,
987
		RenderPrevPosEdge, &ImGuiTraceViewer::gotoPrevPosEdge);
988
	ImGui::SameLine();
989
	gotoStuff("##PrevEdge", "Goto previous edge (positive or negative)",
990
		TRACE_PREV_EDGE, TRACE_ALT_PREV_EDGE,
991
		RenderPrevEdge, &ImGuiTraceViewer::gotoPrevEdge);
992
	ImGui::SameLine();
993
	gotoStuff("##NextEdge", "Goto next edge (positive or negative)",
994
		TRACE_NEXT_EDGE, TRACE_ALT_NEXT_EDGE,
995
		RenderNextEdge, &ImGuiTraceViewer::gotoNextEdge);
996
	ImGui::SameLine();
997
	gotoStuff("##NextPosEdge", "Goto next positive edge",
998
		TRACE_NEXT_POS_EDGE, TRACE_ALT_NEXT_POS_EDGE,
999
		RenderNextPosEdge, &ImGuiTraceViewer::gotoNextPosEdge);
1000
	ImGui::SameLine();
1001
	gotoStuff("##NextNegEdge", "Goto next negative edge",
1002
		TRACE_NEXT_NEG_EDGE, TRACE_ALT_NEXT_NEG_EDGE,
1003
		RenderNextNegEdge, &ImGuiTraceViewer::gotoNextNegEdge);
1004
	HelpMarker("Place a primary (or secondary) marker by clicking (or shift-click) on the graph below.\n"
1005
	           "Advance the markers to the previous/next (positive or negative) edge via the buttons on the left, "
1006
	           "(normal-click for the primary marker, shift-click for the secondary marker).\n"
1007
	           "On the right the position and distance between the markers is shown. "
1008
	           "The units can be selected via the dropdown list in the top-right corner.");
1009
	ImGui::SameLine(0.0f, style.ItemSpacing.x * 2.0f);
1010
1011
	auto unitFactor = getUnitConversionFactor();
1012
	if (timelineOrigin == one_of(Origin::ZERO, Origin::NOW)) {
1013
		ImGui::TextUnformatted("prim."sv);
1014
		simpleToolTip("Primary marker (green). Place via left-mouse-click on the graph.");
1015
		ImGui::SameLine();
1016
		auto leftStr = formatDouble(selectedTime1.toDouble() * unitFactor);
1017
		ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8.0f);
1018
		if (ImGui::InputText("##left", &leftStr)) {
1019
			if (auto d = StringOp::stringTo<double>(leftStr)) {
1020
				selectedTime1 = EmuTime::fromDouble(*d / unitFactor);
1021
			}
1022
		}
1023
		ImGui::SameLine();
1024
		ImGui::TextUnformatted("sec."sv);
1025
		simpleToolTip("Secondary marker (red). Place via shift-left-mouse-click on the graph.");
1026
		ImGui::SameLine();
1027
		auto rightStr = formatDouble(selectedTime2.toDouble() * unitFactor);
1028
		ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8.0f);
1029
		if (ImGui::InputText("##right", &rightStr)) {
1030
			if (auto d = StringOp::stringTo<double>(rightStr)) {
1031
				selectedTime2 = EmuTime::fromDouble(*d / unitFactor);
1032
			}
1033
		}
1034
		ImGui::SameLine();
1035
	}
1036
	ImGui::TextUnformatted("dist."sv);
1037
	simpleToolTip("Distance between primary and secondary markers.");
1038
	ImGui::SameLine();
1039
	im::Disabled(true, [&]{
1040
		auto distStr = formatDouble((selectedTime2.toDouble() - selectedTime1.toDouble()) * unitFactor);
1041
		ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8.0f);
1042
		ImGui::InputText("##dist", &distStr, ImGuiInputTextFlags_ReadOnly);
1043
	});
1044
	ImGui::SameLine();
1045
1046
	ImGui::TextUnformatted("units"sv);
1047
	ImGui::SameLine();
1048
	ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5.0f);
1049
	static_assert(NUM_UNITS == 6);
1050
	std::array shortUnitNames = {
1051
		"sec", "ms", "\u00b5s",
1052
		"cycles", "PAL", "NTSC",
1053
	};
1054
	std::array longUnitNames = {
1055
		"sec (seconds)", "ms (milliseconds)", "\u00b5s (microseconds)",
1056
		"cycles (Z80 @3.57MHz)", "PAL frames (50.16Hz)", "NTSC frames (59.92Hz)",
1057
	};
1058
	im::Combo("##units", shortUnitNames[units], [&]{
1059
		for (int i = 0; i < Units::NUM_UNITS; ++i) {
1060
			if (ImGui::Selectable(longUnitNames[i])) {
1061
				units = i;
1062
			}
1063
		}
1064
	});
1065
}
1066
1067
void ImGuiTraceViewer::drawNames(float rulerHeight, float rowHeight, int mouseRow)
1068
{
1069
	int windowFlags = ImGuiWindowFlags_NoScrollbar
1070
			| ImGuiWindowFlags_NoScrollWithMouse
1071
			| ImGuiWindowFlags_AlwaysHorizontalScrollbar;
1072
	im::Child("##col0", gl::vec2{col0_width, 0.0f}, 0, windowFlags, [&] {
1073
		const auto& style = ImGui::GetStyle();
1074
		const auto& io = ImGui::GetIO();
1075
1076
		auto pos0 = ImGui::GetCursorPos();
1077
		if (ImGui::Button("Select ...")) showSelect = true;
1078
		ImGui::SetCursorPos(pos0);
1079
1080
		ImGui::Dummy({10.0f, rulerHeight});
1081
		auto cPos = ImGui::GetCursorPos();
1082
		ImGui::SetScrollY(scrollY);
1083
1084
		auto* drawList = ImGui::GetWindowDrawList();
1085
		gl::vec2 clipMin = drawList->GetClipRectMin() + gl::vec2{0.0f, rulerHeight};
1086
		gl::vec2 clipMax = drawList->GetClipRectMax();
1087
		bool hovered = (clipMin.x <= io.MousePos.x) && (io.MousePos.x < clipMax.x) &&
1088
		               (clipMin.y <= io.MousePos.y) && (io.MousePos.y < clipMax.y);
1089
1090
		std::optional<int> dragUp;
1091
		std::optional<int> dragDown;
1092
		bool openContext = false;
1093
		drawList->PushClipRect(clipMin, clipMax);
1094
		auto colorText = getColor(imColor::TEXT);
1095
		im::ListClipper(traces.size(), -1, rowHeight, [&](int row) {
1096
			auto tl = clipMin + gl::vec2{0.0f, float(row) * rowHeight};
1097
			bool rowHovered = hovered && (row == mouseRow);
1098
			if (auto color = rowHovered           ? std::optional<ImU32>(ImGui::GetColorU32(ImGuiCol_HeaderHovered))
1099
			               : (row == selectedRow) ? std::optional<ImU32>(ImGui::GetColorU32(ImGuiCol_Header))
1100
			                                      : std::optional<ImU32>{}) {
1101
				drawList->AddRectFilled(tl, tl + gl::vec2{col0_width, rowHeight}, *color);
1102
			}
1103
			const auto* trace = traces[row];
1104
			const auto& name = trace->name;
1105
			drawText(drawList, tl + gl::vec2{0.0f, style.FramePadding.y + style.ItemSpacing.y},
1106
				 colorText, traces[row]->name);
1107
1108
			ImGui::SetCursorPos(cPos + gl::vec2{0.0f, float(row) * rowHeight});
1109
			ImGui::InvisibleButton(name.c_str(), gl::vec2{col0_width, rowHeight});
1110
			if (ImGui::IsItemActive() && !ImGui::IsItemHovered()) {
1111
				auto dy = ImGui::GetMouseDragDelta(0).y;
1112
				auto dist = rowHeight * 0.5f;
1113
				if (dy > dist) {
1114
					dragDown = row;
1115
				} else if (dy < -dist) {
1116
					dragUp = row;
1117
				}
1118
			}
1119
			if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
1120
				selectedRow = row;
1121
			}
1122
			if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
1123
				selectedRow = row;
1124
				openContext = true;
1125
			}
1126
			simpleToolTip(trace->description);
1127
		});
1128
		drawList->PopClipRect();
1129
1130
		auto doSwap = [&](int idx1, int idx2) {
1131
			assert(idx1 < idx2);
1132
			auto name1 = traces[idx1]->name;
1133
			auto name2 = traces[idx2]->name;
1134
			auto it1 = std::ranges::find(selectedTraces, name1, &SelectedTrace::name);
1135
			auto it2 = std::ranges::find(it1 + 1, selectedTraces.end(), name2, &SelectedTrace::name);
1136
			if (it1 != selectedTraces.end() && it2 != selectedTraces.end()) {
1137
				std::iter_swap(it1, it2);
1138
			}
1139
		};
1140
		if (dragUp && *dragUp >= 1) {
1141
			doSwap(*dragUp - 1, *dragUp);
1142
			if (selectedRow == *dragUp) --selectedRow;
1143
		} else if (dragDown && *dragDown < int(traces.size() - 1)) {
1144
			doSwap(*dragDown, *dragDown + 1);
1145
			if (selectedRow == *dragDown) ++selectedRow;
1146
		}
1147
1148
		if (openContext) ImGui::OpenPopup("names-context");
1149
		im::Popup("names-context", [&]{
1150
			auto* trace = getTrace(traces, selectedRow);
1151
			if (!trace) {
1152
				ImGui::CloseCurrentPopup();
1153
				return;
1154
			}
1155
			if (trace->getType() == Tracer::Trace::Type::INTEGER) {
1156
				im::Menu("Format", [&]{
1157
					using enum Tracer::Trace::Format;
1158
					auto format = trace->getFormat();
1159
					if (ImGui::RadioButton("Binary",      format == BIN)) trace->setFormat(BIN);
1160
					if (ImGui::RadioButton("Decimal",     format == DEC)) trace->setFormat(DEC);
1161
					if (ImGui::RadioButton("Hexadecimal", format == HEX)) trace->setFormat(HEX);
1162
				});
1163
				ImGui::Separator();
1164
			}
1165
			if (ImGui::MenuItem("Clear")) {
1166
				trace->clear();
1167
			}
1168
			if (ImGui::MenuItem("Clear all")) {
1169
				for (auto* tr : traces) {
1170
					tr->clear();
1171
				}
1172
			}
1173
		});
1174
	});
1175
}
1176
1177
void ImGuiTraceViewer::drawSplitter(float width)
1178
{
1179
	const auto& io = ImGui::GetIO();
1180
1181
	ImGui::SameLine(0.0f, 0.0f);
1182
1183
	auto pos = ImGui::GetCursorScreenPos();
1184
	auto avail = ImGui::GetContentRegionAvail();
1185
	ImGui::InvisibleButton("##splitter", ImVec2(width, -1));
1186
	bool hovered = ImGui::IsItemHovered();
1187
	bool active = ImGui::IsItemActive();
1188
	if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
1189
		col0_width = std::clamp(col0_width + io.MouseDelta.x, 50.0f, ImGui::GetContentRegionAvail().x - 50.0f);
1190
	}
1191
	if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
1192
1193
	auto x_center = pos.x + (width - 1.0f) * 0.5f;
1194
	auto y0 = pos.y;
1195
	auto y1 = y0 + avail.y;
1196
1197
	auto col_sep = ImGui::GetColorU32(ImGuiCol_Separator);
1198
	auto col_sep_active = ImGui::GetColorU32(ImGuiCol_SeparatorActive);
1199
	auto col_hover = ImGui::GetColorU32(ImGuiCol_SeparatorHovered);
1200
	ImGui::GetWindowDrawList()->AddLine(
1201
		ImVec2(x_center, y0), ImVec2(x_center, y1), active ? col_sep_active : (hovered ? col_hover : col_sep), 1.0f);
1202
1203
	ImGui::SameLine(0.0f, 0.0f);
1204
}
1205
1206
void ImGuiTraceViewer::drawRuler(gl::vec2 size, const Convertor& convertor, EmuTime from, EmuTime to, EmuTime now)
1207
{
1208
	auto labelSize = ImGui::CalcTextSize("0123456789"sv);
1209
	auto numLabels = size.x / labelSize.x;
1210
	auto origin = [&]{
1211
		switch (timelineOrigin) {
1212
		default:
1213
		case Origin::ZERO:      return 0.0;
1214
	        case Origin::PRIMARY:   return selectedTime1.toDouble();
1215
		case Origin::SECONDARY: return selectedTime2.toDouble();
1216
		case Origin::NOW:       return now.toDouble();
1217
		}
1218
	}();
1219
	auto unitFactor = getUnitConversionFactor();
1220
	auto [labels, step] = timelineFormatter.calc(
1221
		from.toDouble() - origin, to.toDouble() - origin, double(numLabels), unitFactor);
1222
	auto minorStep = step * 0.1 / unitFactor;
1223
1224
	auto* drawList = ImGui::GetWindowDrawList();
1225
	auto colorText = getColor(imColor::TEXT);
1226
	auto colorTick = ImGui::GetColorU32(ImGuiCol_Separator);
1227
	auto y0 = size.y * 0.67f;
1228
	auto y1 = size.y * 0.85f;
1229
	auto y2 = size.y;
1230
	gl::vec2 clipMin = drawList->GetClipRectMin();
1231
	gl::vec2 clipMax = drawList->GetClipRectMax();
1232
	drawList->AddLine({clipMin.x, clipMin.y + y0}, {clipMax.x, clipMin.y + y0}, colorTick);
1233
1234
	for (const auto& l : labels) {
1235
		const auto& text = l.text;
1236
		auto value = l.value + origin;
1237
		auto textSize = ImGui::CalcTextSize(text);
1238
		auto centerX = convertor.timeToX(EmuTime::fromDouble(value));
1239
		drawText(drawList, clipMin + gl::vec2{centerX - textSize.x * 0.5f, scrollY}, colorText, text);
1240
		for (auto i : xrange(10)) {
1241
			auto x = convertor.timeToX(EmuTime::fromDouble(value + double(i) * minorStep));
1242
			drawList->AddLine(clipMin + gl::vec2{x, y0}, clipMin + gl::vec2{x, (i % 5) == 0 ? y2 : y1}, colorTick);
1243
		}
1244
	}
1245
}
1246
1247
static EmuTime snapToEvent(EmuTime time, auto convertor, std::span<const Tracer::Event> events)
1248
{
1249
	auto [it, dist] = find_closest(events, time, {}, &Tracer::Event::time);
1250
	if (it == events.end()) return time;
1251
	auto h5 = ImGui::GetFrameHeight() * 0.20f;
1252
	return (dist < convertor.deltaXtoDuration(h5)) ? it->time : time;
1253
}
1254
static EmuTime snapToEvent(float mouseX, auto convertor, std::span<const Tracer::Event> events)
1255
{
1256
	auto time = convertor.xToTime(mouseX);
1257
	return snapToEvent(time, convertor, events);
1258
}
1259
1260
void ImGuiTraceViewer::drawGraphs(float rulerHeight, float rowHeight, int mouseRow, const Convertor& convertor,
1261
                                  EmuTime minT, EmuTime maxT, EmuTime now, MSXMotherBoard& motherBoard)
1262
{
1263
	if        (ImGui::Shortcut(ImGuiMod_Shift | ImGuiKey_LeftArrow, ImGuiInputFlags_Repeat)) {
1264
		viewStartTime = viewStartTime.saturateSubtract(convertor.deltaXtoDuration(0.5f * convertor.viewScreenWidth));
1265
		scrollChanged = true;
1266
	} else if (ImGui::Shortcut(ImGuiMod_Shift | ImGuiKey_RightArrow, ImGuiInputFlags_Repeat)) {
1267
		viewStartTime += convertor.deltaXtoDuration(0.5f * convertor.viewScreenWidth);
1268
		scrollChanged = true;
1269
	} else if (ImGui::Shortcut(ImGuiKey_LeftArrow, ImGuiInputFlags_Repeat)) {
1270
		viewStartTime = viewStartTime.saturateSubtract(convertor.deltaXtoDuration(12.0f));
1271
		scrollChanged = true;
1272
	} else if (ImGui::Shortcut(ImGuiKey_RightArrow, ImGuiInputFlags_Repeat)) {
1273
		viewStartTime += convertor.deltaXtoDuration(12.0f);
1274
		scrollChanged = true;
1275
	}
1276
1277
	auto totalT = maxT - minT;
1278
	auto total1000 = totalT * 1e-3;
1279
	auto [scrollWidth, scrollPos] = [&]{
1280
		// if we make the scroll bar range TOO wide, ImGui has precision issues
1281
		if (total1000 < viewDuration) {
1282
			auto scrollSlackDuration = (viewDuration < totalT) ? (totalT - viewDuration) : EmuDuration::zero();
1283
			auto width = float(scrollSlackDuration.div(viewDuration)) + 1.0f;
1284
			auto pos = float((viewStartTime - minT).div(viewDuration));
1285
			return std::pair(width, pos);
1286
		} else {
1287
			auto width = 1000.0f;
1288
			auto pos = float((viewStartTime - minT).div(total1000));
1289
			return std::pair(width, pos);
1290
		}
1291
	}();
1292
	float fakeScrollWidth = scrollWidth * convertor.viewScreenWidth;
1293
	float fakeScrollPos = scrollPos * convertor.viewScreenWidth;
1294
	// make sure it's always possible to scroll to the future (scrollbar is never in the rightmost position)
1295
	fakeScrollWidth = std::max(fakeScrollWidth, fakeScrollPos + convertor.viewScreenWidth + 5);
1296
	ImGui::SetNextWindowContentSize({fakeScrollWidth, rulerHeight + rowHeight * float(traces.size())});
1297
	ImGui::SetNextWindowScroll({fakeScrollPos, scrollY});
1298
1299
	int windowFlags2 = ImGuiWindowFlags_HorizontalScrollbar
1300
	                 | ImGuiWindowFlags_AlwaysHorizontalScrollbar;
1301
	im::Child("Events", {}, {}, windowFlags2, [&] {
1302
		scrollY = ImGui::GetScrollY();
1303
		auto scrollX = ImGui::GetScrollX();
1304
		if (std::abs(scrollX - fakeScrollPos) > 0.5f) {
1305
			// user changed scroll position
1306
			auto dur = std::max(total1000, viewDuration);
1307
			viewStartTime = minT + (dur * double(scrollX / convertor.viewScreenWidth));
1308
			scrollChanged = true;
1309
		}
1310
1311
		const auto& style = ImGui::GetStyle();
1312
		const auto& io = ImGui::GetIO();
1313
1314
		auto t0 = convertor.xToTime(0.0f);
1315
		auto t1 = convertor.xToTime(convertor.viewScreenWidth);
1316
1317
		drawRuler({convertor.viewScreenWidth, rulerHeight}, convertor, t0, t1, now);
1318
		ImGui::Dummy({10.0f, rulerHeight});
1319
1320
		auto* drawList = ImGui::GetWindowDrawList();
1321
		gl::vec2 clipMin = drawList->GetClipRectMin() + gl::vec2{0.0f, rulerHeight};
1322
		gl::vec2 clipMax = drawList->GetClipRectMax();
1323
		bool hovered = (clipMin.x <= io.MousePos.x) && (io.MousePos.x < clipMax.x) &&
1324
		               (clipMin.y <= io.MousePos.y) && (io.MousePos.y < clipMax.y) &&
1325
		               ImGui::IsWindowHovered();
1326
		auto mouseX = io.MousePos.x - clipMin.x;
1327
1328
		drawList->PushClipRect(clipMin, clipMax);
1329
		im::ListClipperID(traces.size(), rowHeight, [&](int row) {
1330
			const auto& trace = *traces[row];
1331
			auto it0 = std::ranges::lower_bound(trace.events, viewStartTime, {}, &Tracer::Event::time);
1332
			if (it0 != trace.events.begin()) --it0;
1333
			auto graphEndTime = viewStartTime + viewDuration;
1334
			auto it1 = std::ranges::upper_bound(trace.events, graphEndTime, {}, &Tracer::Event::time);
1335
			std::span<const Tracer::Event> visibleEvents(it0, it1);
1336
1337
			bool rowHovered = hovered && (row == mouseRow);
1338
			if (rowHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1339
				if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) {
1340
					selectedTime2 = snapToEvent(mouseX, convertor, visibleEvents);
1341
				} else {
1342
					selectedRow = row;
1343
					selectedTime1 = snapToEvent(mouseX, convertor, visibleEvents);
1344
				}
1345
			}
1346
			auto tl = clipMin + gl::vec2{0.0f, float(row) * rowHeight};
1347
			if (auto color = rowHovered           ? std::optional<ImU32>(ImGui::GetColorU32(ImGuiCol_HeaderHovered))
1348
			               : (row == selectedRow) ? std::optional<ImU32>(ImGui::GetColorU32(ImGuiCol_Header))
1349
			                                      : std::optional<ImU32>{}) {
1350
				drawList->AddRectFilled(tl, {clipMax.x, tl.y + rowHeight}, *color);
1351
			}
1352
			tl.y += style.ItemSpacing.y;
1353
			switch (trace.getType()) {
1354
			case Tracer::Trace::Type::MONOSTATE:
1355
				drawEventsVoid(tl, convertor, maxT, visibleEvents, rowHovered);
1356
				break;
1357
			case Tracer::Trace::Type::BOOL:
1358
				drawEventsBool(tl, convertor, maxT, visibleEvents, rowHovered);
1359
				break;
1360
			default:
1361
				drawEventsValue(tl, convertor, maxT, visibleEvents, rowHovered, trace.getFormat());
1362
			}
1363
		});
1364
		auto drawLine = [&](EmuTime t, ImU32 color) {
1365
			if (t != EmuTime::infinity()) {
1366
				auto x = float(convertor.timeToX(t) + clipMin.x);
1367
				drawList->AddLine({x, clipMin.y}, {x, clipMax.y}, color);
1368
			}
1369
		};
1370
		drawLine(selectedTime1, IM_COL32(0, 255, 0, 255)); // green
1371
		drawLine(selectedTime2, IM_COL32(255, 0, 0, 255)); // red
1372
1373
		if (now < maxT) {
1374
			auto nowX = convertor.timeToX(now);
1375
			auto x = nowX + clipMin.x;
1376
			drawList->AddRectFilled({x, clipMin.y}, clipMax, IM_COL32(0, 0, 0, 128));
1377
1378
			if (viewStartTime <= now && now <= (viewStartTime + viewDuration)) {
1379
				if ((now - EmuTime::zero()).toDouble() >= (viewDuration.toDouble() * 0.99)) {
1380
					prevNowX = nowX;
1381
				}
1382
			}
1383
		}
1384
1385
		drawList->PopClipRect();
1386
1387
		if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
1388
			ImGui::OpenPopup("graph-context");
1389
			ctxMouseX = mouseX;
1390
			ctxMouseRow = mouseRow;
1391
		}
1392
		if (hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) {
1393
			auto delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Middle);
1394
			ImGui::ResetMouseDragDelta(ImGuiMouseButton_Middle);
1395
			ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
1396
			if (delta.x < 0.0f) {
1397
				viewStartTime += convertor.deltaXtoDuration(-delta.x);
1398
			} else if (delta.x > 0.0f) {
1399
				viewStartTime = viewStartTime.saturateSubtract(convertor.deltaXtoDuration(delta.x));
1400
			}
1401
			scrollChanged = true;
1402
		}
1403
1404
		im::Popup("graph-context", [&]{
1405
			ImGui::TextUnformatted("Scroll to");
1406
			im::Indent([&]{
1407
				if (ImGui::MenuItem("Primary")) {
1408
					scrollTo(selectedTime1);
1409
				}
1410
				if (ImGui::MenuItem("Secondary")) {
1411
					scrollTo(selectedTime2);
1412
				}
1413
				ImGui::AlignTextToFramePadding();
1414
				ImGui::TextUnformatted("Position");
1415
				ImGui::SameLine();
1416
				bool valid = StringOp::stringTo<double>(ctxInputPos).has_value();
1417
				im::StyleColor(!valid, ImGuiCol_Text, getColor(imColor::ERROR), [&]{
1418
					ImGui::SetNextItemWidth(6.0f * ImGui::GetFontSize());
1419
					if (ImGui::InputText("##pos", &ctxInputPos, ImGuiInputTextFlags_EnterReturnsTrue)) {
1420
						if (auto d = StringOp::stringTo<double>(ctxInputPos)) {
1421
							auto unitFactor = getUnitConversionFactor();
1422
							scrollTo(EmuTime::fromDouble(*d / unitFactor));
1423
							ImGui::CloseCurrentPopup();
1424
						}
1425
					}
1426
				});
1427
			});
1428
			ImGui::Separator();
1429
			const auto& reverseManager = motherBoard.getReverseManager();
1430
			bool reverseEnabled = reverseManager.isCollecting();
1431
			im::Disabled(!reverseEnabled, [&]{
1432
				if (ImGui::MenuItem("Reverse emulation to here")) {
1433
					auto time = convertor.xToTime(ctxMouseX);
1434
					if (const auto* trace = getTrace(traces, ctxMouseRow)) {
1435
						time = snapToEvent(time, convertor, trace->events);
1436
					}
1437
					manager.executeDelayed(makeTclList("reverse", "goto", time.toDouble()));
1438
				}
1439
				simpleToolTip(reverseEnabled
1440
					? "Rewind/forward emulation till this position in the timeline.\n"
1441
					  "Note: during replay NO new trace information is collected."
1442
					: "Requires 'reverse' to be enabled. Enable via 'Save state > Reverse/replay settings > Enable reverse/replay'");
1443
			});
1444
		});
1445
	});
1446
}
1447
1448
void ImGuiTraceViewer::paint(MSXMotherBoard* motherBoard)
1449
{
1450
	if (!motherBoard) return;
1451
1452
	if (show) {
1453
		ImGui::SetNextWindowSize(gl::vec2{68, 29} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1454
		int flags = showMenuBar ? ImGuiWindowFlags_MenuBar : 0;
1455
		im::Window("Probe/Trace Viewer", &show, flags, [&] {
1456
			paintMain(*motherBoard);
1457
		});
1458
	}
1459
	if (showSelect) {
1460
		ImGui::SetNextWindowSize(gl::vec2{18, 27} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1461
		im::Window("Select probes and traces", &showSelect, [&] {
1462
			paintSelect(*motherBoard);
1463
		});
1464
	}
1465
	if (showHelp) {
1466
		ImGui::SetNextWindowSize(gl::vec2{50, 38} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
1467
		im::Window("Trace Viewer Help", &showHelp, [&] {
1468
			paintHelp();
1469
		});
1470
	}
1471
}
1472
1473
const std::vector<Tracer::Trace*>& ImGuiTraceViewer::getTraces(MSXMotherBoard& motherBoard)
1474
{
1475
	auto& debugger = motherBoard.getDebugger();
1476
	auto& tracer = debugger.getTracer();
1477
	calcTraces(tracer);
1478
	return traces;
1479
}
1480
1481
void ImGuiTraceViewer::calcTraces(Tracer& tracer)
1482
{
1483
	const auto& allTraces = tracer.getTraces();
1484
1485
	for (const auto& trace : allTraces) {
1486
		bool isProbe = !trace->isUserTrace();
1487
		if (!isProbe || allUserTraces) {
1488
			// TODO this is O(N^2), ok?
1489
			if (!contains(selectedTraces, trace->name, &SelectedTrace::name)) {
1490
				selectedTraces.emplace_back(trace->name, isProbe);
1491
			}
1492
		}
1493
	}
1494
	traces.clear();
1495
	for (const auto& sel : selectedTraces) {
1496
		if (allUserTraces || sel.selected) {
1497
			if (auto it = std::ranges::find(allTraces, sel.name, &Tracer::Trace::name);
1498
			it != allTraces.end()) {
1499
				traces.push_back(it->get());
1500
			}
1501
		}
1502
	}
1503
}
1504
1505
void ImGuiTraceViewer::paintMain(MSXMotherBoard& motherBoard)
1506
{
1507
	auto& debugger = motherBoard.getDebugger();
1508
	auto& tracer = debugger.getTracer();
1509
	calcTraces(tracer);
1510
1511
	if (ImGui::Shortcut(ImGuiKey_UpArrow, ImGuiInputFlags_Repeat)) {
1512
		if (selectedRow > 0) --selectedRow;
1513
	} else if (ImGui::Shortcut(ImGuiKey_DownArrow, ImGuiInputFlags_Repeat)) {
1514
		if (selectedRow < int(traces.size() - 1)) ++selectedRow;
1515
	}
1516
1517
	EmuTime minT = EmuTime::infinity();
1518
	EmuTime maxT = EmuTime::zero();
1519
	if (timelineStart || timelineStop) {
1520
		for (const auto& trace : traces) {
1521
			if (trace->events.empty()) continue;
1522
			minT = std::min(minT, trace->events.front().time);
1523
			maxT = std::max(maxT, trace->events.back().time);
1524
		}
1525
		if (minT > maxT) minT = maxT;
1526
	}
1527
1528
	static constexpr auto margin = EmuDuration::sec(0.02);
1529
	auto now = motherBoard.getCurrentTime();
1530
	minT = timelineStart ? minT : EmuTime::zero();
1531
	minT = std::min(minT, viewStartTime);
1532
	maxT = timelineStop ? maxT : now;
1533
	maxT = std::max(maxT, viewStartTime + viewDuration);
1534
	maxT += margin; // add margin at end
1535
	if (maxT < minT) maxT = minT + margin;
1536
	auto totalT = maxT - minT;
1537
1538
	auto viewScreenWidth = ImGui::GetContentRegionAvail().x - col0_width - splitterWidth;
1539
1540
	if (followNow && !scrollChanged && prevNowX > 0.0f) {
1541
		// IF 'follow-now' feature enabled
1542
		// AND user didn't manually scroll/zoom previous frame
1543
		// AND 'now' was visible the previous frame
1544
		// THEN auto-scroll so that 'now' remains visible (at the same X location).
1545
		auto pixelsPerTick = viewScreenWidth / float(viewDuration.toUint64());
1546
		auto d = EmuDuration(uint64_t(prevNowX / pixelsPerTick));
1547
		viewStartTime = now.saturateSubtract(d);
1548
	}
1549
	prevNowX = -1.0f;
1550
	scrollChanged = false;
1551
1552
	if (showMenuBar) {
1553
		im::MenuBar([&]{
1554
			drawMenuBar(minT, totalT, tracer);
1555
		});
1556
	}
1557
	drawToolBar(minT, totalT, viewScreenWidth);
1558
	ImGui::Separator();
1559
1560
	const auto& io = ImGui::GetIO();
1561
	const auto& style = ImGui::GetStyle();
1562
	float rulerHeight = ImGui::GetTextLineHeight() * 1.5f;
1563
	auto rowHeight = ImGui::GetFrameHeight() + 2.0f * style.ItemSpacing.y;
1564
	auto mouseY = io.MousePos.y - ImGui::GetCursorScreenPos().y + scrollY - rulerHeight;
1565
	auto mouseRow = static_cast<int>(mouseY / rowHeight);
1566
	if (mouseRow < 0 || mouseRow >= int(traces.size())) mouseRow = -1;
1567
1568
	drawNames(rulerHeight, rowHeight, mouseRow);
1569
	drawSplitter(splitterWidth);
1570
	Convertor convertor{viewStartTime, viewDuration, viewScreenWidth};
1571
	drawGraphs(rulerHeight, rowHeight, mouseRow, convertor, minT, maxT, now, motherBoard);
1572
}
1573
1574
void ImGuiTraceViewer::paintSelect(MSXMotherBoard& motherBoard)
1575
{
1576
	auto& debugger = motherBoard.getDebugger();
1577
	auto& tracer = debugger.getTracer();
1578
	const auto& allTraces = tracer.getTraces();
1579
1580
	const auto& style = ImGui::GetStyle();
1581
	auto rightSpace = ImGui::CalcTextSize("(?)").x + style.ItemSpacing.x;
1582
	auto boxHeight = ImGui::GetFrameHeightWithSpacing() * 4.7f;
1583
1584
	ImGui::SeparatorText("Probes");
1585
	im::Child("##probes", {-rightSpace, boxHeight}, ImGuiChildFlags_Borders, [&]{
1586
		auto& allProbes = debugger.getProbes();
1587
		im::ListClipper(allProbes.size(), [&](int i) {
1588
			const auto& probe = *allProbes[i];
1589
			const auto& name = probe.getName();
1590
			bool selected = contains(allTraces, name, &Tracer::Trace::name);
1591
			if (ImGui::Checkbox(name.c_str(), &selected)) {
1592
				if (selected) {
1593
					tracer.selectProbe(debugger, name);
1594
					if (!contains(selectedTraces, name, &SelectedTrace::name)) {
1595
						selectedTraces.emplace_back(name, true);
1596
					}
1597
				} else {
1598
					tracer.unselectProbe(debugger, name);
1599
					if (auto it = std::ranges::find(selectedTraces, name, &SelectedTrace::name);
1600
						it != selectedTraces.end()) {
1601
						selectedTraces.erase(it);
1602
					}
1603
				}
1604
			}
1605
			simpleToolTip(probe.getDescription());
1606
		});
1607
	});
1608
	HelpMarker(
1609
		"Probes are provided by openMSX (builtin). "
1610
		"The available probes depend on the selected MSX machine and extensions.\n"
1611
		"Some probes generate A LOT of data, therefor there's no option to automatically enable all.");
1612
1613
	ImGui::SeparatorText("User traces");
1614
	if (ImGui::RadioButton("Auto select all", allUserTraces)) {
1615
		allUserTraces = true;
1616
	}
1617
	HelpMarker("Automatically select all user defined traces. Including when more traces are created later.");
1618
	if (ImGui::RadioButton("Manual select", !allUserTraces)) {
1619
		allUserTraces = false;
1620
	}
1621
	im::DisabledIndent(allUserTraces, [&]{
1622
		im::Child("##traces", {-rightSpace, boxHeight}, ImGuiChildFlags_Borders, [&]{
1623
			std::vector<zstring_view> userTraceNames;
1624
			userTraceNames.reserve(allTraces.size());
1625
			for (const auto& trace : allTraces) {
1626
				if (trace->isUserTrace()) {
1627
					userTraceNames.emplace_back(trace->name);
1628
				}
1629
			}
1630
			im::ListClipper(userTraceNames.size(), [&](int i) {
1631
				const auto& name = userTraceNames[i];
1632
				auto it1 = std::ranges::find(selectedTraces, name, &SelectedTrace::name);
1633
				bool selected = it1 != selectedTraces.end() && it1->selected;
1634
				if (ImGui::Checkbox(name.c_str(), &selected)) {
1635
					if (selected) {
1636
						if (auto it = std::ranges::find(selectedTraces, name, &SelectedTrace::name);
1637
							it != selectedTraces.end()) {
1638
							it->selected = true;
1639
						} else {
1640
							selectedTraces.emplace_back(std::string(name), true);
1641
						}
1642
					} else {
1643
						if (auto it = std::ranges::find(selectedTraces, name, &SelectedTrace::name);
1644
							it != selectedTraces.end()) {
1645
							it->selected = false;
1646
						}
1647
					}
1648
				}
1649
				simpleToolTip([&]{
1650
					auto* trace = tracer.findTrace(name); assert(trace);
1651
					return trace->description;
1652
				});
1653
			});
1654
		});
1655
		HelpMarker(
1656
			"Traces are created by the user, via the 'debug trace add' command. "
1657
			"Typically trace data is added via breakpoints or watchpoints.\n"
1658
			"For example in the breakpoint editor set a breakpoint on your function, "
1659
			"and then in the action column (possibly unhide via right-click the column header) "
1660
			"enter the command 'debug trace add myFunction [reg A]'. "
1661
			"This will add a trace point whenever your function is called, "
1662
			"and it will log the value of register A");
1663
	});
1664
}
1665
1666
static inline void RenderParagraph(std::string_view text)
1667
{
1668
	im::TextWrapPos([&]{
1669
		ImGui::TextUnformatted(text);
1670
	});
1671
}
1672
1673
static inline void RenderBullet(std::string_view text)
1674
{
1675
	ImGui::Bullet();
1676
	ImGui::SameLine();
1677
	RenderParagraph(text);
1678
}
1679
1680
static inline void TextUnformattedUnderlined(std::string_view text)
1681
{
1682
	ImGui::TextUnformatted(text);
1683
	auto size = ImGui::CalcTextSize(text);
1684
	auto p = ImGui::GetItemRectMin();
1685
	auto start = gl::vec2{p.x, p.y + size.y + 2.0f};
1686
	auto end = gl::vec2{p.x + size.x, start.y};
1687
	auto color = getColor(imColor::TEXT);
1688
	ImGui::GetWindowDrawList()->AddLine(start, end, color);
1689
	ImGui::NewLine();
1690
}
1691
1692
void ImGuiTraceViewer::paintHelp()
1693
{
1694
	if (helpSection == HelpSection::OVERVIEW) {
1695
		ImGui::SetScrollHereY(0.0f);
1696
	}
1697
	TextUnformattedUnderlined("Overview");
1698
	RenderParagraph(
1699
		"The 'Probe/Trace Viewer' is an openMSX debugging tool to visualize events "
1700
		"(and optionally associated data) on a timeline. This can help answer "
1701
		"questions like:");
1702
	RenderBullet(
1703
		"How much time passed between the VDP generating a VBLANK IRQ and the Z80 responding?");
1704
	RenderBullet(
1705
		"When is the VDP busy executing commands? Can I change my code to increase "
1706
		"utilization (less idle time)?");
1707
	RenderBullet(
1708
		"When is my function called and what parameters were passed?");
1709
1710
	RenderParagraph(
1711
		"First, you need to tell openMSX what information to collect. This can be in the "
1712
		"form of builtin openMSX 'probes' or user-defined 'traces'. See the section "
1713
		"'Probes and traces' for more details. In some cases collecting this information "
1714
		"can be expensive. That's why openMSX does not collect everything by default "
1715
		"(which would also clutter the view).");
1716
	RenderParagraph(
1717
		"Next, run your test case. During this process you'll see the graphs being "
1718
		"created. Typically, after some interesting data has been collected you'll "
1719
		"want to pause emulation and examine the graph in detail. See the 'Graphs' "
1720
		"section.");
1721
	RenderParagraph(
1722
		"The main purpose of this tool is to analyze the collected data. Often this "
1723
		"involves cross-correlating multiple data sources. For example, how many "
1724
		"cycles are there between entering 'myFuncA' and starting the next VDP "
1725
		"command? When was the last IRQ before 'myFuncB'? See the 'Navigation' "
1726
		"section.");
1727
	RenderParagraph(
1728
		"If this is still a bit abstract, you can read the 'Example' section for a "
1729
		"more concrete example.");
1730
	ImGui::NewLine();
1731
	ImGui::NewLine();
1732
1733
	if (helpSection == HelpSection::PROBES_AND_TRACES) {
1734
		ImGui::SetScrollHereY(0.0f);
1735
	}
1736
	TextUnformattedUnderlined("Probes and Traces");
1737
	RenderParagraph(
1738
		"As mentioned in the 'Overview' section, the first step is to select the "
1739
		"information sources. This is done via the 'Select probes and traces' window. "
1740
		"That window can be opened via 'menu bar > File > Select probes and traces', "
1741
		"or by clicking the 'Select...' button in the left column.");
1742
	RenderParagraph(
1743
		"This window has two main parts: 'Probes' and 'User traces'. The 'Probes' part "
1744
		"lists builtin openMSX probes with checkboxes to enable collection. The 'User "
1745
		"traces' part offers 'Auto select all' or 'Manual select'. 'Manual select' shows "
1746
		"currently defined user traces with checkboxes. 'Auto select all' selects all "
1747
		"user traces, including ones not yet created (and thus not yet visible in "
1748
		"the list).");
1749
	RenderParagraph(
1750
		"Both probes and traces generate timestamped events. Some events only record a "
1751
		"timestamp (for example 'z80.accept.IRQ'). Other events carry extra information: "
1752
		"'VDP.commandExecuting' also carries a boolean indicating whether a command "
1753
		"started or stopped. User traces may carry arbitrary additional data (boolean, "
1754
		"integer, float, string), for example function parameters.");
1755
	RenderParagraph(
1756
		"Probes are builtin low-level events provided by openMSX; traces are "
1757
		"user-defined and typically project-specific. Because some builtin probes "
1758
		"generate many events or are expensive to collect, enable only those you "
1759
		"need; traces are usually smaller and tailored to your debugging needs.");
1760
	RenderParagraph(
1761
		"openMSX currently provides generic probes for IRQs and the VDP command "
1762
		"engine, providing low-level information not otherwise exposed via "
1763
		"debuggables. Future openMSX versions may add more builtin probes.");
1764
	RenderParagraph(
1765
		"Traces are user-defined and often attached to breakpoints or watchpoints. "
1766
		"Create a trace data point with the Tcl command:");
1767
	im::Font(manager.fontMono, [&]{ im::Indent([&]{ RenderParagraph(
1768
		"debug trace add <trace-name> [<value>]"); }); });
1769
	RenderParagraph(
1770
		"Execute 'help debug trace' and 'help debug trace add' in the console for "
1771
		"details. Here's a full example, attach to a breakpoint and log register A as data:");
1772
	im::Font(manager.fontMono, [&]{ im::Indent([&]{ RenderParagraph(
1773
		"debug breakpoint create -address 0x1234 -command {debug trace add "
1774
		"myFunction [reg A]}"); }); });
1775
	RenderParagraph(
1776
		"This logs a data point for the trace named 'myFunction' whenever the breakpoint at address 0x1234 "
1777
		"triggers. Using 'debug trace add ...' as the breakpoint '-command' records the trace "
1778
		"instead of stopping emulation. The '[reg A]' expression logs the current value "
1779
		"of the Z80 register A.");
1780
	RenderParagraph(
1781
		"You can create the breakpoint via the above Tcl command or via the GUI "
1782
		"(Debugger > Breakpoints > Editor). Enter the action in the 'Action' column "
1783
		"(unhide that column from the table header context menu if needed).");
1784
	ImGui::NewLine();
1785
	ImGui::NewLine();
1786
1787
	if (helpSection == HelpSection::GRAPHS) {
1788
		ImGui::SetScrollHereY(0.0f);
1789
	}
1790
	TextUnformattedUnderlined("Graphs");
1791
	RenderParagraph(
1792
		"Once probes/traces are selected and data has been collected (by running "
1793
		"a test case), the results appear in the main area of the Probe/Trace "
1794
		"Viewer.");
1795
	RenderParagraph(
1796
		"The left column lists probe/trace names, the right area shows the "
1797
		"timeline and the recorded data.");
1798
	RenderParagraph(
1799
		"There are three data types, each rendered differently:");
1800
	RenderBullet(
1801
		"Event-only: timestamped events without extra data, shown as small up-arrows.");
1802
	RenderBullet(
1803
		"Boolean: timestamped 0/1 transitions, drawn as a square wave (edges at transitions).");
1804
	RenderBullet(
1805
		"Value traces: events carrying integers, floats, or strings; shown as "
1806
		"elongated hexagons that span until the next value, with the value text "
1807
		"inside.");
1808
1809
	RenderParagraph(
1810
		"By default the graph order follows the selection order, but drag names in the "
1811
		"left column to reorder.");
1812
	RenderParagraph(
1813
		"If the collected information does not fit on screen, zoom or scroll to inspect it:");
1814
	RenderBullet(
1815
		"Zoom: via 'View > Zoom', toolbar icons, or shortcuts (default: Ctrl-=, Ctrl--, Ctrl-0).");
1816
	RenderBullet(
1817
		"Scroll: via bottom scrollbar or Left/Right cursor keys (use Shift + Left/Right for faster movement).");
1818
1819
	RenderParagraph("Markers:");
1820
	RenderBullet("Primary marker (green): left-click near an edge to place or snap it.");
1821
	RenderBullet("Secondary marker (red): shift-left-click near an edge to place or snap it.");
1822
1823
	RenderParagraph(
1824
		"Near the top are two text fields showing the marker positions, edit "
1825
		"them to move the markers. A third read-only field shows the distance "
1826
		"between the markers. Use the units dropdown to choose seconds or Z80 "
1827
		"clock cycles.");
1828
	RenderParagraph(
1829
		"Use 'View > Timeline > start/stop' to show the full run (from emulation start to current time) "
1830
		"or only the interval containing collected events. Showing only the "
1831
		"interval with events is useful when debugging a short section of a "
1832
		"long-running program.");
1833
	RenderParagraph(
1834
		"Use 'View > Timeline > Origin' to place the origin at start of emulation (this gives "
1835
		"stable time points) or at the primary marker (this gives shorter time values).");
1836
	ImGui::NewLine();
1837
	ImGui::NewLine();
1838
1839
	if (helpSection == HelpSection::NAVIGATION) {
1840
		ImGui::SetScrollHereY(0.0f);
1841
	}
1842
	TextUnformattedUnderlined("Navigation");
1843
	RenderParagraph(
1844
		"An important feature of the 'Probe/Trace Viewer' is measuring and relating "
1845
		"events across traces (for example, measuring distances or the ordering "
1846
		"between events). The UI offers the following helpers.");
1847
	RenderParagraph(
1848
		"(Shift-)left-click near an edge snaps the primary (or secondary) "
1849
		"marker to that edge, allowing precise distance measurements between "
1850
		"events.");
1851
	RenderParagraph("Navigation functions (six total):");
1852
	RenderBullet("Go to next/previous (any) edge");
1853
	RenderBullet("Go to next/previous positive (=rising) edge");
1854
	RenderBullet("Go to next/previous negative (=falling) edge");
1855
1856
	RenderParagraph(
1857
		"Positive/negative are only relevant for boolean traces: a positive edge is "
1858
		"a transition from low (0) to high (1); a negative edge is the opposite. "
1859
		"For example, in VDP.commandExecuting a positive edge marks when a "
1860
		"command starts and the negative edge when it finishes.");
1861
	RenderParagraph(
1862
		"These functions are available as six toolbar icons. Left-click an icon to "
1863
		"move the primary marker; shift-left-click to move the secondary marker. "
1864
		"Keyboard shortcuts are available (default: Ctrl+A,S,D,F,G,H, six "
1865
		"consecutive keys, same order as the toolbar icons). Ctrl alone moves "
1866
		"the primary marker; Ctrl+Shift moves the secondary marker.");
1867
	RenderParagraph(
1868
		"You can relate events across graphs using the navigation functions. For "
1869
		"example, locate a rising edge in 'VDP.IRQvertical' (when the VDP asserts the "
1870
		"VBLANK IRQ). Select the 'z80.acceptIRQ' row without moving the marker (click "
1871
		"the row name or use the up/down cursor keys), then go to the next event to "
1872
		"see when the Z80 accepted the interrupt (the Z80 will jump to the ISR, "
1873
		"typically at address 0x0038). Selecting 'VDP.IRQvertical' again and advancing to "
1874
		"the next edge shows when the ISR cleared the VDP IRQ.");
1875
	ImGui::NewLine();
1876
	ImGui::NewLine();
1877
1878
	if (helpSection == HelpSection::EXAMPLE) {
1879
		ImGui::SetScrollHereY(0.0f);
1880
	}
1881
	TextUnformattedUnderlined("Example");
1882
	RenderParagraph(
1883
		"Hopefully the other help sections have made the purpose of the 'Probe/Trace "
1884
		"Viewer' clear. But if it's still too abstract then here's an example. To "
1885
		"avoid dependencies on external MSX software, we'll analyze the MSX-BASIC "
1886
		"startup sequence.");
1887
	RenderParagraph(
1888
		"First we need to do some preparation:");
1889
	RenderBullet(
1890
		"Start a Philips NMS 8255 machine (other machines work too but may need slight adjustments).");
1891
	RenderBullet(
1892
		"Pause emulation quickly (before or during the boot logo) so you have "
1893
		"time to set up probes and traces.");
1894
	RenderParagraph(
1895
		"Open the openMSX console (press F10 or 'Tools > Show console') and enter:");
1896
	im::Font(manager.fontMono, [&]{ im::Indent([&]{ RenderParagraph(
1897
		"debug breakpoint create -address 0x0919 -command {debug trace add chput "
1898
		"[format \"%c (%d)\" [reg a] [reg a]]}"); }); });
1899
	RenderParagraph(
1900
		"Alternatively, create the breakpoint via 'Debugger > Breakpoints > Editor': "
1901
		"click 'Add', enter '0x0919' in the 'Address' column, and put the 'debug trace "
1902
		"add ...' command in the 'Action' column (unhide that column if needed). 0x0919 "
1903
		"is the CHPUT BIOS function entry used by MSX-BASIC (MSX-BASIC calls this "
1904
		"address directly rather than the stable entry point at 0x00A2). The 'debug "
1905
		"trace add' command records a data point for the user-defined 'chput' trace; "
1906
		"the 'format' expression prints register 'A' both as a character and as a "
1907
		"decimal integer.");
1908
	RenderParagraph(
1909
		"Open the Probe/Trace Viewer (Debugger > Probe/Trace viewer) and click "
1910
		"'Select...' in the left column (or use 'File > Select probes and traces').");
1911
	RenderParagraph(
1912
		"In the selector enable 'VDP.IRQvertical' and 'z80.acceptIRQ' (and any other "
1913
		"probes you're interested in). In the 'User traces' part, enable 'Auto select all' so your "
1914
		"'chput' trace will appear once it is recorded.");
1915
	RenderParagraph(
1916
		"Now resume emulation and wait for MSX-BASIC to finish booting, then pause again.");
1917
	RenderParagraph("Inspecting the data:");
1918
	RenderBullet(
1919
		"Use Zoom to fit ('toolbar > magnifier' or 'Ctrl-0') for an overview.");
1920
	RenderBullet(
1921
		"Zoom into the dense region using the toolbar or by hovering the mouse "
1922
		"over the region and using Ctrl + mouse wheel.");
1923
	RenderBullet(
1924
		"At sufficient zoom you'll see individual printed characters and their values (e.g. M (77)).");
1925
	RenderBullet(
1926
		"Place the primary marker at the start of the 'M' event (left-click near "
1927
		"the edge); place the secondary marker at the end of the 'M' event "
1928
		"(shift-left-click).");
1929
	RenderParagraph(
1930
		"The dist field in the top bar shows the time between markers. In this "
1931
		"case printing 'M' took 0.00043 seconds; switching units to Z80 cycles shows "
1932
		"1553 cycles; CHPUT is relatively slow.");
1933
	RenderParagraph(
1934
		"At 9.92 seconds printing 'o' took much longer (about 8458 cycles). The "
1935
		"z80.acceptIRQ row shows an interrupt at 9.921 seconds; the ISR consumed "
1936
		"roughly 6900 cycles, explaining the longer duration.");
1937
	RenderParagraph(
1938
		"This simple example demonstrates how visualizing and correlating traces can "
1939
		"reveal timing and interaction details between subsystems.");
1940
1941
	helpSection = {};
1942
}
1943
1944
} // namespace openmsx