OXYGENHUB
◆ main openmsx / src / imgui / ImGuiRasterViewer.cc
27.4 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "ImGuiRasterViewer.hh"
2
3
#include "ImGuiTraceViewer.hh"
4
5
#include "MSXMotherBoard.hh"
6
#include "RawFrame.hh"
7
#include "VDP.hh"
8
9
#include "ImGuiCpp.hh"
10
11
#include "MemBuffer.hh"
12
#include "gl_vec.hh"
13
#include "one_of.hh"
14
#include "ranges.hh"
15
#include "stl.hh"
16
#include "subrange_between.hh"
17
#include "unreachable.hh"
18
19
#include <imgui.h>
20
21
#include <algorithm>
22
#include <cassert>
23
24
namespace openmsx {
25
26
using namespace std::literals;
27
28
using Pixel = uint32_t;
29
static constexpr auto PIXELS_PER_LINE = VDP::TICKS_PER_LINE / 2;
30
static constexpr auto VISIBLE_PIXELS_PER_LINE = 28 + 512 + 30;
31
static_assert(VISIBLE_PIXELS_PER_LINE % 2 == 0, "must be even");
32
static constexpr auto FIRST_VISIBLE_LINE = 3 + 13;
33
static constexpr auto FIRST_VISIBLE_X = (100 + 102) / 2;
34
35
ImGuiRasterViewer::ImGuiRasterViewer(ImGuiManager& manager_, ImGuiTraceViewer& traceViewer_)
36
	: ImGuiPart(manager_)
37
	, traceViewer(traceViewer_)
38
{
39
}
40
41
void ImGuiRasterViewer::save(ImGuiTextBuffer& buf)
42
{
43
	savePersistent(buf, *this, persistentElements);
44
}
45
46
void ImGuiRasterViewer::loadLine(std::string_view name, zstring_view value)
47
{
48
	loadOnePersistent(name, value, *this, persistentElements);
49
}
50
51
void ImGuiRasterViewer::initPixelBuffer(size_t lines)
52
{
53
	size_t size = lines * VISIBLE_PIXELS_PER_LINE;
54
	if (pixelBuffer.size() == size) return;
55
	pixelBuffer.resize(size);
56
}
57
58
[[nodiscard]] static std::span<Pixel, VISIBLE_PIXELS_PER_LINE> getVisibleLinePart(
59
	std::span<Pixel> pixels, size_t line)
60
{
61
	assert(line >= FIRST_VISIBLE_LINE);
62
	line -= FIRST_VISIBLE_LINE;
63
	return subspan<VISIBLE_PIXELS_PER_LINE>(pixels, line * VISIBLE_PIXELS_PER_LINE);
64
}
65
66
static unsigned copyLine(const RawFrame& src, int openMsxLine, std::span<Pixel, VISIBLE_PIXELS_PER_LINE> dst)
67
{
68
	// About the source line:
69
	// - OpenMSX renders lines of width 320 (or 640) pixels, but the real VDP
70
	//   renders 14/15 (or 28/30) border pixels left/right plus 256 (or 512) display
71
	//   pixels.
72
	// - This means the openMSX lines contains 18 (or 36) pixels too many on both sides.
73
	// - With horizontal adjust there can be a shift between left and right border.
74
	// About the destination line:
75
	// - We want to display all 1368 cycles (corresponding to 684 pixels).
76
	// - A line is subdivided in these parts:
77
	//   *  100 cycles ( 25    or  50   pixels): sync
78
	//   *  102 cycles ( 25.5  or  51   pixels): left erase
79
	//   *   56 cycles ( 14    or  28   pixels): left border
80
	//   * 1024 cycles (256    or 512   pixels): display area
81
	//   *   59 cycles ( 14.75 or  29.5 pixels): right border
82
	//   *   27 cycles (  6.75 or  13.5 pixels): right erase
83
	// - We render 684 pixels, so skip 101 pixels, 28 border pixels, 512
84
	//   display pixels, 30 border pixels, and 13 pixels remaining.
85
	int line = std::clamp(openMsxLine, 0, 239);
86
	auto srcLine = src.getLineDirect(line);
87
	auto width = src.getLineWidthDirect(line);
88
	switch (width) {
89
	case 1:
90
		std::ranges::fill(dst, srcLine[0]);
91
		break;
92
	case 320:
93
		for (size_t i = 0; i < (VISIBLE_PIXELS_PER_LINE / 2); ++i) {
94
			dst[2 * i + 0] = srcLine[i + 18];
95
			dst[2 * i + 1] = srcLine[i + 18];
96
		}
97
		break;
98
	case 640:
99
		copy_to_range(subspan<VISIBLE_PIXELS_PER_LINE>(srcLine, 36), dst);
100
		break;
101
	default:
102
		UNREACHABLE;
103
	}
104
	return width;
105
}
106
107
void ImGuiRasterViewer::paint(MSXMotherBoard* motherBoard)
108
{
109
	if (!show) return;
110
	if (!motherBoard) return;
111
112
	ImGui::SetNextWindowSize({700, 722}, ImGuiCond_FirstUseEver);
113
	im::Window("Raster beam viewer", &show, [&]{
114
		im::TreeNode("Settings", ImGuiTreeNodeFlags_DefaultOpen, [&]{
115
			paintSettings();
116
		});
117
		ImGui::Separator();
118
		paintDisplay(*motherBoard);
119
	});
120
121
	if (showConfigure) {
122
		ImGui::SetNextWindowSize(gl::vec2{19, 26} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
123
		im::Window("Raster beam - configure traces", &showConfigure, [&]{
124
			paintConfigure(*motherBoard);
125
		});
126
	}
127
}
128
129
static const int MAX_ZOOM = 8;
130
void ImGuiRasterViewer::paintSettings()
131
{
132
	ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.0f);
133
	ImGui::Combo("Zoom", &zoomSelect, "1x\0002x\0003x\0004x\0005x\0006x\0007x\0008x\000");
134
	assert(MAX_ZOOM == 8);
135
136
	ImGui::SameLine();
137
	ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
138
	ImGui::SameLine();
139
140
	ImGui::Checkbox("Grid", &showGrid);
141
	ImGui::SameLine();
142
	im::Disabled(!showGrid, [&]{
143
		ImGui::ColorEdit4("Grid color", gridColor.data(),
144
			ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
145
	});
146
	HelpMarker("At zoom 1x the grid isn't shown for hi-res screen modes (e.g. screen 7)");
147
148
	ImGui::SameLine();
149
	ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
150
	ImGui::SameLine();
151
152
	ImGui::Checkbox("Fade out", &showFadeOut);
153
	ImGui::SameLine();
154
	im::Disabled(!showFadeOut, [&]{
155
		ImGui::ColorEdit4("FadeOut color", fadeOutColor.data(),
156
			ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
157
	});
158
	HelpMarker("Similar to the 'beam position': shows the current position "
159
	           "of the CRT raster beam, but now by fading out 'older' colors");
160
161
	ImGui::Checkbox("Beam position", &showBeam);
162
	ImGui::SameLine();
163
	im::Disabled(!showBeam, [&]{
164
		ImGui::ColorEdit4("Beam color", beamColor.data(),
165
			ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
166
	});
167
	HelpMarker("Shows the current position of the CRT raster beam. "
168
	           "In general this is only useful when emulation is paused, "
169
	           "e.g. when stepping through the code with the debugger.");
170
171
	ImGui::SameLine();
172
	ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
173
	ImGui::SameLine();
174
175
	ImGui::Checkbox("VBlank IRQ", &showVblankIrq);
176
	ImGui::SameLine();
177
	im::Disabled(!showVblankIrq, [&]{
178
		ImGui::ColorEdit4("VBlank color", vblankIrqColor.data(),
179
			ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
180
	});
181
	HelpMarker("Shows the position where the vblank-IRQ will occur (when enabled)");
182
183
	ImGui::SameLine();
184
	ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
185
	ImGui::SameLine();
186
187
	ImGui::Checkbox("Line IRQ", &showLineIrq);
188
	ImGui::SameLine();
189
	im::Disabled(!showLineIrq, [&]{
190
		ImGui::ColorEdit4("Line color", lineIrqColor.data(),
191
			ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
192
	});
193
	HelpMarker("Shows the position where the line-IRQ will occur (when enabled)");
194
195
	ImGui::SameLine();
196
	ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
197
	ImGui::SameLine();
198
199
	ImGui::Checkbox("Traces", &showTraces);
200
	ImGui::SameLine();
201
	im::Disabled(!showTraces, [&]{
202
		if (ImGui::Button("Configure...")) {
203
			showConfigure = true;
204
		}
205
	});
206
	HelpMarker("Shows trace events (Debugger > Probe/Trace viewer) that happened the last frame");
207
}
208
209
ImGuiRasterViewer::TraceInfo& ImGuiRasterViewer::getTraceInfoFor(std::string_view name)
210
{
211
	auto it = std::ranges::lower_bound(traceInfos, name, {}, &TraceInfo::name);
212
	if ((it != traceInfos.end()) && (it->name == name)) {
213
		return *it;
214
	}
215
	auto [r, g, b] = generateDistinctColor(narrow_cast<unsigned>(traceInfos.size()));
216
	return *traceInfos.emplace(it, std::string(name), gl::vec4{r, g, b, 1.0f}, true);
217
}
218
219
void ImGuiRasterViewer::paintDisplay(MSXMotherBoard& motherBoard)
220
{
221
	auto* vdp = dynamic_cast<VDP*>(motherBoard.findDevice("VDP")); // TODO name based OK?
222
	if (!vdp) return;
223
224
	EmuTime time = vdp->getCurrentTime();
225
	const RawFrame* work = vdp->getWorkingFrame(time);
226
	const RawFrame* last = vdp->getLastFrame();
227
	if (!work || !last) return;
228
	assert(work->getHeight() == 240);
229
	assert(last->getHeight() == 240);
230
231
	auto timeToXY = [&](EmuTime t) -> std::pair<int, int> {
232
		int ticks = vdp->getTicksThisFrame(t);
233
		return {ticks % VDP::TICKS_PER_LINE,
234
		        ticks / VDP::TICKS_PER_LINE};
235
	};
236
	auto [x, y] = timeToXY(time);
237
238
	// Openmsx renders into a 240 line RawFrame (212 or 192 display + borders).
239
	// That means:
240
	// - For neutral vertical adjust (0), there are 14 border lines at the top,
241
	//   212 display lines, and 14 border lines at the bottom.
242
	// - With vertical adjust in the top most position, the display
243
	//   area shifts 7 pixels to the top, so 7 + 212 + 21.
244
	// - With vertical adjust in the bottom most position, the display
245
	//   area shifts 8 pixels to the bottom, so 22 + 212 + 6.
246
	// - In case of only 192 display lines, there are 20 more border lines,
247
	//   10 above and 10 below.
248
	bool pal = vdp->isPalTiming();
249
	int numLines = pal ? VDP::PAL_LINES : VDP::NTSC_LINES;
250
	auto frameDuration = vdp->getFrameDuration();
251
252
	int zoom = zoomSelect + 1;
253
	gl::vec2 fullSize{float(PIXELS_PER_LINE), float(2 * numLines)};
254
	gl::vec2 zoomedFullSize = fullSize * float(zoom);
255
256
	auto availSize = gl::vec2(ImGui::GetContentRegionAvail()) - gl::vec2(0.0f, ImGui::GetTextLineHeightWithSpacing());
257
	auto reqSize = zoomedFullSize + gl::vec2(ImGui::GetStyle().ScrollbarSize);
258
	gl::vec2 scrnPos;
259
260
	std::array<unsigned, VDP::NUM_LINES_MAX> allLineWidths;
261
	std::ranges::fill(allLineWidths, 1); // default to border width
262
263
	gl::vec2 closestPos{FLT_MAX};
264
	im::Child("##display", min(availSize, reqSize), {}, ImGuiWindowFlags_HorizontalScrollbar, [&]{
265
		scrnPos = ImGui::GetCursorScreenPos();
266
267
		auto makeAndBindTex = [&](gl::Texture& tex) {
268
			if (!tex.get()) {
269
				tex = gl::Texture(false, true); // no interpolation, with wrapping
270
			}
271
			tex.bind();
272
		};
273
274
		gl::vec4 checkerBoardColor1{0.2f, 0.2f, 0.2f, 0.8f}; // RGBA
275
		gl::vec4 checkerBoardColor2{0.4f, 0.4f, 0.4f, 0.8f}; // RGBA
276
		auto col1 = ImGui::ColorConvertFloat4ToU32(checkerBoardColor1);
277
		auto col2 = ImGui::ColorConvertFloat4ToU32(checkerBoardColor2);
278
		std::array<Pixel, 4> pixels = {col1, col2, col2, col1};
279
		makeAndBindTex(checkerTex);
280
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0,
281
			GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
282
283
		// align checkerboard pattern with MSX pixel (0,0) (when set-adjust is 0)
284
		static constexpr float checkerBoardSize = 8.0f;
285
		static constexpr gl::vec2 texOffset = gl::vec2{-1.0f, 10.0f} / (4.0f * checkerBoardSize);
286
		ImGui::Image(checkerTex.getImGui(), zoomedFullSize,
287
		             texOffset, texOffset + fullSize / (4.0f * checkerBoardSize));
288
289
		static constexpr int visibleLinesPal = 43 + 212 + 39;
290
		static constexpr int visibleLinesNtsc = 16 + 212 + 15;
291
		int numVisibleLines = pal ? visibleLinesPal : visibleLinesNtsc;
292
		initPixelBuffer(numVisibleLines);
293
		auto lineWidths = std::span(allLineWidths).subspan(FIRST_VISIBLE_LINE, numVisibleLines);
294
		bool any320 = false;
295
		bool any640 = false;
296
		auto setWidth = [&](int i, unsigned width) {
297
			lineWidths[i] = width;
298
			if (width == 320) any320 = true;
299
			if (width == 640) any640 = true;
300
		};
301
302
		int firstOpenMsxLine = FIRST_VISIBLE_LINE + ((pal ? 43 : 16) - 14);
303
		for (auto i : xrange(numVisibleLines)) {
304
			auto absLine = i + FIRST_VISIBLE_LINE;
305
			auto dst = getVisibleLinePart(pixelBuffer, absLine);
306
			int openMsxLine = absLine - firstOpenMsxLine;
307
			if (absLine < y) {
308
				auto w = copyLine(*work, openMsxLine, dst);
309
				setWidth(i, w);
310
			} else if (absLine > y) {
311
				auto w = copyLine(*last, openMsxLine, dst);
312
				setWidth(i, w);
313
			} else {
314
				std::array<Pixel, VISIBLE_PIXELS_PER_LINE> left;
315
				std::array<Pixel, VISIBLE_PIXELS_PER_LINE> right;
316
				auto wl = copyLine(*work, openMsxLine, left);
317
				auto wr = copyLine(*last, openMsxLine, right);
318
				setWidth(i, std::max(wl, wr));
319
320
				int xx = (x - (100 + 102)) / 2;
321
				if (wl <= 320) xx &= ~1; // round down to even
322
				int n = std::clamp(xx, 0, VISIBLE_PIXELS_PER_LINE);
323
324
				copy_to_range(subspan(left, 0, n), dst.subspan(0, n));
325
				copy_to_range(subspan(right, n),   dst.subspan(n));
326
			}
327
		}
328
		// Set border lines (width=1):
329
		//  to 640 if there are lines with width=640 and there are none with width=320,
330
		//  else to 320
331
		int borderWidth = any640 && !any320 ? 640 : 320;
332
		if (borderWidth == 320) any320 = true;
333
		std::ranges::replace(allLineWidths, 1, borderWidth);
334
335
		makeAndBindTex(viewTex);
336
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VISIBLE_PIXELS_PER_LINE, numVisibleLines, 0,
337
				GL_RGBA, GL_UNSIGNED_BYTE, pixelBuffer.data());
338
339
		auto* drawList = ImGui::GetWindowDrawList();
340
		auto colorForLine = [&](int line) {
341
			auto color = [&] -> gl::vec4 {
342
				static constexpr gl::vec4 opaque(1.0f);
343
				if (!showFadeOut) return opaque;
344
				float t = float(line - y) / float(numLines);
345
				return ImLerp(ImVec4(fadeOutColor), ImVec4(opaque), t - std::floor(t));
346
			}();
347
			return ImGui::ColorConvertFloat4ToU32(color);
348
		};
349
		auto topColor = colorForLine(FIRST_VISIBLE_LINE);
350
		auto bottomLine = FIRST_VISIBLE_LINE + numVisibleLines;
351
		auto bottomColor = colorForLine(bottomLine);
352
		auto topLeft = scrnPos + float(zoom) * gl::vec2{float(FIRST_VISIBLE_X), float(FIRST_VISIBLE_LINE * 2)};
353
		auto bottomRight = topLeft + float(zoom) * gl::vec2{float(VISIBLE_PIXELS_PER_LINE), float(2 * numVisibleLines)};
354
		if (y < FIRST_VISIBLE_LINE || y >= bottomLine) {
355
			ImGui::AddImageRectMultiColor(
356
				drawList, viewTex.getImGui(),
357
				topLeft, bottomRight,
358
				gl::vec2{0.0f, 0.0f}, gl::vec2{1.0f, 1.0f},
359
				topColor, topColor, bottomColor, bottomColor);
360
		} else {
361
			auto middleVertexY = scrnPos.y + float(2 * zoom * y);
362
			auto middleTexY = float(y - FIRST_VISIBLE_LINE) / float(numVisibleLines);
363
			Pixel opaque = 0xffffffff;
364
365
			// top gradient
366
			if (y != FIRST_VISIBLE_LINE) {
367
				ImGui::AddImageRectMultiColor(
368
					drawList, viewTex.getImGui(),
369
					topLeft, gl::vec2{bottomRight.x, middleVertexY},
370
					gl::vec2{0.0f, 0.0f}, gl::vec2{1.0f, middleTexY},
371
					topColor, topColor, opaque, opaque);
372
			}
373
			// middle line
374
			int x1 = (x / 2) - FIRST_VISIBLE_X;
375
			if (allLineWidths[y] == 320) x1 &= ~1; // round down to even
376
			auto x2 = std::clamp(topLeft.x + float(zoom * x1), topLeft.x, bottomRight.x);
377
			auto t2 = std::clamp(float(x1) / float(VISIBLE_PIXELS_PER_LINE), 0.0f, 1.0f);
378
			auto middleVertexY1 = middleVertexY + float(2 * zoom);
379
			auto middleTexY1 = float((y + 1) - FIRST_VISIBLE_LINE) / float(numVisibleLines);
380
			drawList->AddImage(
381
				viewTex.getImGui(),
382
				gl::vec2{topLeft.x, middleVertexY}, gl::vec2{x2, middleVertexY1},
383
				gl::vec2{0.0f, middleTexY}, gl::vec2{t2, middleTexY1},
384
				opaque);
385
			Pixel transparent = colorForLine(y);
386
			drawList->AddImage(
387
				viewTex.getImGui(),
388
				gl::vec2{x2, middleVertexY}, gl::vec2{bottomRight.x, middleVertexY1},
389
				gl::vec2{t2, middleTexY}, gl::vec2{1.0f, middleTexY1},
390
				transparent);
391
			// bottom gradient
392
			if (y != (bottomLine - 1)) {
393
				ImGui::AddImageRectMultiColor(
394
					drawList, viewTex.getImGui(),
395
					gl::vec2{topLeft.x, middleVertexY1}, bottomRight,
396
					gl::vec2{0.0f, middleTexY1}, gl::vec2{1.0f, 1.0f},
397
					transparent, transparent, bottomColor, bottomColor);
398
			}
399
		}
400
401
		if (showGrid) {
402
			std::array<Pixel, (2 * MAX_ZOOM) * (2 * MAX_ZOOM)> pixels_;
403
			auto color = ImGui::ColorConvertFloat4ToU32(gridColor);
404
			auto makeGridUnit = [&](int nx, int ny) {
405
				auto pixels2 = std::span(pixels_).subspan(0, nx * ny);
406
				for (auto yy : xrange(ny)) {
407
					auto line = pixels2.subspan(yy * nx, nx);
408
					for (auto xx : xrange(nx)) {
409
						line[xx] = (xx == 0 || yy == 0) ? color : 0;
410
					}
411
				}
412
			};
413
			if (any320) {
414
				int nx = 2 * zoom;
415
				int ny = 2 * zoom;
416
				makeGridUnit(nx, ny);
417
				makeAndBindTex(gridTex320);
418
				glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, nx, ny, 0,
419
					GL_RGBA, GL_UNSIGNED_BYTE, pixels_.data());
420
			}
421
			if (any640 && (zoom > 1)) {
422
				int nx = zoom;
423
				int ny = 2 * zoom;
424
				makeGridUnit(nx, ny);
425
				makeAndBindTex(gridTex640);
426
				glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, nx, ny, 0,
427
					GL_RGBA, GL_UNSIGNED_BYTE, pixels_.data());
428
			}
429
			foreach_run(lineWidths, [&](size_t i, size_t n, unsigned width) {
430
				assert(width == one_of(320u, 640u));
431
				if (width == 640 && (zoom <= 1)) return;
432
				auto numPixels = VISIBLE_PIXELS_PER_LINE / ((width == 320) ? 2 : 1);
433
				drawList->AddImage(
434
					((width == 320) ? gridTex320 : gridTex640).getImGui(),
435
					gl::vec2{topLeft.x, topLeft.y + float(2 * zoom * i)},
436
					gl::vec2{bottomRight.x, topLeft.y + float(2 * zoom * (i + n))},
437
					gl::vec2{}, gl::vec2{float(numPixels), float(n)});
438
			});
439
		}
440
441
		float closestDist = FLT_MAX;
442
		std::string closestText;
443
444
		auto addAnnotationXY = [&](auto textFunc, gl::vec4 color, gl::ivec2 vdpPos) {
445
			gl::vec2 center = drawCrosshair(vdpPos, color, scrnPos, zoom, allLineWidths);
446
			gl::vec2 mouse = ImGui::GetIO().MousePos;
447
			auto dist2 = length2(mouse - center); // distance squared
448
			if (dist2 <= closestDist) {
449
				if (center == closestPos) {
450
					strAppend(closestText, '\n', textFunc());
451
				} else {
452
					closestDist = dist2;
453
					closestText = textFunc();
454
					closestPos = center;
455
				}
456
			}
457
		};
458
		auto timeToVdpPos = [&](EmuTime tt) {
459
			auto [xx, yy] = timeToXY(tt);
460
			return gl::ivec2{xx, yy % numLines};
461
		};
462
		auto addAnnotation = [&](auto textFunc, gl::vec4 color, EmuTime tt) {
463
			addAnnotationXY(textFunc, color, timeToVdpPos(tt));
464
		};
465
466
		if (showBeam) {
467
			addAnnotationXY([]{ return "Raster beam position (now)"; }, beamColor, {x, y});
468
		}
469
		if (showVblankIrq) {
470
			if (auto vTime = vdp->getVScanTime()) {
471
				addAnnotation([]{ return "Next VBLANK-IRQ position (future)"; }, vblankIrqColor, *vTime);
472
			}
473
		}
474
		if (showLineIrq) {
475
			if (auto hTime = vdp->getHScanTime()) {
476
				addAnnotation([]{ return "Next line-IRQ position (future)"; }, lineIrqColor, *hTime);
477
			}
478
		}
479
		if (showTraces) {
480
			EmuTime from = time.saturateSubtract(frameDuration);
481
			EmuTime to = time;
482
			const auto& traces = traceViewer.getTraces(motherBoard);
483
			for (const auto* trace : traces) {
484
				const auto& traceInfo = getTraceInfoFor(trace->name);
485
				if (!traceInfo.enabled) continue;
486
				auto subEvents = subrange_between(trace->events, from, to, {}, &Tracer::Event::time);
487
				for (const auto& event : subEvents) {
488
					auto tt = event.time + frameDuration; // bring event into the 'future' (doesn't change screen position)
489
					auto print = [&]{
490
						std::array<char, 64> buf;
491
						auto valStr = ImGuiTraceViewer::formatTraceValue(event.value, buf, trace->getFormat());
492
						return strCat(trace->name, strCat_if(!valStr.empty(), ": ", valStr));
493
					};
494
					addAnnotation(print, traceInfo.color, tt);
495
				}
496
			}
497
			auto m1 = traceViewer.getMarker(0);
498
			auto m2 = traceViewer.getMarker(1);
499
			if (showMarkers) {
500
				auto draw = [&](EmuTime tt, gl::vec4 color) {
501
					if ((tt < from) || (tt >= to)) return;
502
					tt += frameDuration; // bring into the 'future' (doesn't change screen position)
503
					drawMarker(timeToVdpPos(tt), color, scrnPos, zoom, allLineWidths);
504
				};
505
				draw(m1, markerColor1);
506
				draw(m2, markerColor2);
507
508
			}
509
			if (shadeType == Shade::MARKERS) {
510
				// intersection of between-markers-interval and last-frame-interval
511
				auto [mMin, mMax] = std::minmax(m1, m2);
512
				auto mStart = std::max(mMin, from);
513
				auto mStop  = std::min(mMax, to);
514
				if (mStart < mStop) {
515
					drawRegion(
516
						timeToVdpPos(mStart + frameDuration), timeToVdpPos(mStop + frameDuration),
517
						shadeColor, scrnPos, zoom, std::span(allLineWidths.data(), numLines));
518
				}
519
			} else if (shadeType == Shade::TRACE) {
520
				if (auto t_it = std::ranges::find(traces, shadeTrace, &Tracer::Trace::name);
521
				    t_it != traces.end()) {
522
					const auto& trace = **t_it;
523
524
					auto it0 = std::ranges::lower_bound(trace.events, from, {}, &Tracer::Event::time);
525
					if (it0 != trace.events.begin()) --it0;
526
					auto it1 = std::ranges::upper_bound(trace.events, to, {}, &Tracer::Event::time);
527
528
					for (auto it = it0; it != it1; ++it) {
529
						bool skip = it->value.visit(overloaded{
530
							[](std::monostate) { return true; },
531
							[](uint64_t i) { return i == 0; },
532
							[](double d) { return d == 0.0; },
533
							[](std::string_view s) { return s.empty(); }
534
						});
535
						if (skip) continue;
536
						auto mStart = std::max(from, it->time);
537
						auto next = it + 1;
538
						auto mStop = std::min(to, (next == it1) ? to : next->time);
539
						drawRegion(
540
							timeToVdpPos(mStart + frameDuration), timeToVdpPos(mStop + frameDuration),
541
							shadeColor, scrnPos, zoom, std::span(allLineWidths.data(), numLines));
542
					}
543
				}
544
			}
545
		}
546
		if (closestDist < (6.0f * 6.0f)) {
547
			im::Tooltip([&]{
548
				auto [tx, vy] = trunc((closestPos - scrnPos) / (gl::vec2(0.5f, 2.0f) * float(zoom)));
549
				int mx = (tx - vdp->getLeftSprites()) / 2;
550
				int my = vy - vdp->getLineZero();
551
				strAppend(closestText, "\nMSX coordinates: x=", mx, " y=", my);
552
				ImGui::TextUnformatted(closestText);
553
			});
554
		} else {
555
			closestPos = gl::vec2{FLT_MAX};
556
		}
557
	});
558
	if (ImGui::IsItemHovered()) {
559
		auto [tx, vy] = trunc((gl::vec2(ImGui::GetIO().MousePos) - scrnPos) / (gl::vec2(0.5f, 2.0f) * float(zoom)));
560
		if (0 <= tx && tx < VDP::TICKS_PER_LINE &&
561
		    0 <= vy && vy < numLines) {
562
			int vx = tx / 2;
563
			int mx = (tx - vdp->getLeftSprites()) / 2;
564
			int my = vy - vdp->getLineZero();
565
			if (allLineWidths[vy] == 320) {
566
				vx >>= 1;
567
				mx >>= 1; // note: NOT the same as 'mx / 2' for negative values
568
			}
569
570
			auto dec3 = [&](int d) {
571
				im::ScopedFont sf(manager.fontMono);
572
				ImGui::SameLine(0.0f, 0.0f);
573
				ImGui::Text("%3d", d);
574
			};
575
576
			ImGui::TextUnformatted("MSX coordinates: x="sv); dec3(mx);
577
			ImGui::SameLine();
578
			ImGui::TextUnformatted("y="sv); dec3(my);
579
580
			ImGui::SameLine(0.0f, 20.0f);
581
			ImGui::TextUnformatted("Absolute VDP coordinates: x="sv); dec3(vx);
582
			ImGui::SameLine();
583
			ImGui::TextUnformatted("line="sv); dec3(vy);
584
585
			if (showMarkers && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
586
				if (closestPos.x < FLT_MAX) {
587
					auto [xx, yy] = trunc((closestPos - scrnPos) / (gl::vec2(0.5f, 2.0f) * float(zoom)));
588
					tx = xx;
589
					vy = yy;
590
				}
591
				int ticks = vy * VDP::TICKS_PER_LINE + tx;
592
				auto tt = vdp->getTimeInFrame(ticks);
593
				if (tt > time) tt -= frameDuration;
594
595
				bool shift = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
596
				traceViewer.setMarker(shift ? 1 : 0, tt);
597
			}
598
		}
599
	}
600
}
601
602
[[nodiscard]] static auto vdpToScreenCoord(
603
	gl::ivec2 vdpPos, gl::vec2 scrnTopLeft, int zoom, std::span<const unsigned> lineWidths)
604
{
605
	struct Result {
606
		gl::vec2 center;
607
		gl::vec2 halfPixel = {0.5f, 1.0f};
608
	} result;
609
610
	auto xx = vdpPos.x / 2;
611
	auto cx = 0.5f;
612
	if (lineWidths[vdpPos.y] == 320) {
613
		xx = (xx + 1) & ~1; // round up to even
614
		result.halfPixel.x = 1.0f;
615
		cx = 0.25f;
616
	}
617
	gl::vec2 rasterBeamPos(float(xx), float(vdpPos.y) * 2.0f);
618
	result.halfPixel *= float(zoom);
619
	result.center = scrnTopLeft + (gl::vec2(rasterBeamPos) + gl::vec2{cx, 1.0f}) * float(zoom);
620
	return result;
621
}
622
623
gl::vec2 ImGuiRasterViewer::drawCrosshair(
624
	gl::ivec2 vdpPos, gl::vec4 color_, gl::vec2 scrnTopLeft, int zoom,
625
	std::span<const unsigned, VDP::NUM_LINES_MAX> lineWidths)
626
{
627
	auto [center, halfPixel] = vdpToScreenCoord(vdpPos, scrnTopLeft, zoom, lineWidths);
628
629
	auto thickness = float(zoom) * 0.5f;
630
	gl::vec2 zm = halfPixel;
631
	auto zm1 = 1.5f * zm;
632
	auto zm3 = 3.5f * zm;
633
	auto* drawList = ImGui::GetWindowDrawList();
634
	auto color = ImGui::ColorConvertFloat4ToU32(color_);
635
	drawList->AddRect(center - zm, center + zm, color, 0.0f, thickness);
636
	drawList->AddLine(center - gl::vec2{zm1.x, 0.0f}, center - gl::vec2{zm3.x, 0.0f}, color, thickness);
637
	drawList->AddLine(center + gl::vec2{zm1.x, 0.0f}, center + gl::vec2{zm3.x, 0.0f}, color, thickness);
638
	drawList->AddLine(center - gl::vec2{0.0f, zm1.y}, center - gl::vec2{0.0f, zm3.y}, color, thickness);
639
	drawList->AddLine(center + gl::vec2{0.0f, zm1.y}, center + gl::vec2{0.0f, zm3.y}, color, thickness);
640
	return center;
641
}
642
643
void ImGuiRasterViewer::drawMarker(
644
	gl::ivec2 vdpPos, gl::vec4 color_, gl::vec2 scrnTopLeft, int zoom,
645
	std::span<const unsigned, VDP::NUM_LINES_MAX> lineWidths)
646
{
647
	auto [center, _] = vdpToScreenCoord(vdpPos, scrnTopLeft, zoom, lineWidths);
648
649
	auto* drawList = ImGui::GetWindowDrawList();
650
	auto radius = float(zoom) * 4.0f;
651
	auto color = ImGui::ColorConvertFloat4ToU32(color_);
652
	auto thickness = float(zoom) * 2.0f;
653
	drawList->AddCircle(center, radius, color, {}, thickness);
654
}
655
656
static void drawRegion3(
657
	gl::ivec2 from, gl::ivec2 to, ImU32 color, gl::vec2 scrnTopLeft, int zoom,
658
	std::span<const unsigned> lineWidths)
659
{
660
	assert(from.x <= to.x);
661
	assert(from.y <= to.y);
662
	auto [center1, halfPixel1] = vdpToScreenCoord(from, scrnTopLeft, zoom, lineWidths);
663
	auto [center2, halfPixel2] = vdpToScreenCoord(to,   scrnTopLeft, zoom, lineWidths);
664
	auto* drawList = ImGui::GetWindowDrawList();
665
	drawList->AddRectFilled(center1 - halfPixel1, center2 + halfPixel2, color);
666
}
667
668
static void drawRegion2(
669
	gl::ivec2 from, gl::ivec2 to, ImU32 color, gl::vec2 scrnTopLeft, int zoom,
670
	std::span<const unsigned> lineWidths)
671
{
672
	assert(from.y <= to.y);
673
	if (from.y == to.y) {
674
		// single line
675
		drawRegion3(from, to, color, scrnTopLeft, zoom, lineWidths);
676
	} else {
677
		// split in 2 or 3
678
		drawRegion3(from, {VDP::TICKS_PER_LINE, from.y}, color, scrnTopLeft, zoom, lineWidths);
679
		if ((from.y + 1) < to.y) {
680
			drawRegion3({0, from.y + 1}, {VDP::TICKS_PER_LINE, to.y - 1}, color, scrnTopLeft, zoom, lineWidths);
681
		}
682
		drawRegion3({0, to.y}, to, color, scrnTopLeft, zoom, lineWidths);
683
	}
684
}
685
686
void ImGuiRasterViewer::drawRegion(
687
	gl::ivec2 from, gl::ivec2 to, gl::vec4 color_, gl::vec2 scrnTopLeft, int zoom,
688
	std::span<const unsigned> lineWidths)
689
{
690
	auto color = ImGui::ColorConvertFloat4ToU32(color_);
691
	if (std::tie(from.y, from.x) > std::tie(to.y, to.x)) {
692
		// split in 2
693
		drawRegion2(from, {VDP::TICKS_PER_LINE, narrow<int>(lineWidths.size() - 1)},
694
		            color, scrnTopLeft, zoom, lineWidths);
695
		drawRegion2({0, 0}, to,
696
		            color, scrnTopLeft, zoom, lineWidths);
697
	} else {
698
		drawRegion2(from, to,
699
		            color, scrnTopLeft, zoom, lineWidths);
700
	}
701
}
702
703
void ImGuiRasterViewer::paintConfigure(MSXMotherBoard& motherBoard)
704
{
705
	ImGui::SeparatorText("Probes / traces");
706
	if (ImGui::Button("Select probes/traces...")) {
707
		traceViewer.showSelect = true;
708
	}
709
	HelpMarker("Select which probe and trace information will be collected.\n"
710
	           "In some cases this can be expensive, so it's not done by default.");
711
712
	const auto& traces = traceViewer.getTraces(motherBoard);
713
	auto n = std::clamp(float(traces.size()), 1.0f, 5.5f);
714
	im::Child("##list", {0, n * ImGui::GetFrameHeightWithSpacing()}, [&]{
715
		if (traces.empty()) {
716
			ImGui::TextDisabled("No traces selected");
717
		} else {
718
			im::ListClipperID(traces.size(), [&](int i) {
719
				const auto* trace = traces[i];
720
				auto& traceInfo = getTraceInfoFor(trace->name);
721
722
				ImGui::ColorEdit4("##color", traceInfo.color.data(),
723
					ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
724
				ImGui::SameLine();
725
				ImGui::Checkbox(traceInfo.name.c_str(), &traceInfo.enabled);
726
			});
727
		}
728
	});
729
730
	ImGui::SeparatorText("Markers");
731
	ImGui::Checkbox("Draw markers", &showMarkers);
732
	HelpMarker("Show the 'Probe/Trace Viewer' markers also in the 'Raster Beam Viewer' window.");
733
	im::DisabledIndent(!showMarkers, [&]{
734
		ImGui::ColorEdit4("Primary", markerColor1.data(),
735
			ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_AlphaBar);
736
		HelpMarker("Place via left-mouse-click in the 'Raster Beam Viewer' screen area.");
737
		ImGui::ColorEdit4("Secondary", markerColor2.data(),
738
			ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_AlphaBar);
739
		HelpMarker("Place via shift-left-mouse-click in the 'Raster Beam Viewer' screen area.");
740
	});
741
742
	ImGui::SeparatorText("Shade");
743
	ImGui::RadioButton("Disabled", &shadeType, Shade::DISABLED);
744
	ImGui::RadioButton("Between markers", &shadeType, Shade::MARKERS);
745
	HelpMarker("Color the area between the primary and secondary marker.");
746
	ImGui::RadioButton("Trace", &shadeType, Shade::TRACE);
747
	HelpMarker("Color the areas where a specific trace is active.");
748
	im::DisabledIndent(shadeType != Shade::TRACE, [&]{
749
		const char* preview =
750
			traces.empty() ? "no traces selected" :
751
			contains(traces, shadeTrace, &Tracer::Trace::name) ? shadeTrace.c_str() :
752
			"none";
753
		ImGui::SetNextItemWidth(-FLT_MIN);
754
		im::Combo("##trace", preview, [&]{
755
			im::ListClipperID(traces.size(), [&](int i) {
756
				const auto* trace = traces[i];
757
				if (ImGui::Selectable(trace->name.c_str())) {
758
					shadeTrace = trace->name;
759
				}
760
			});
761
		});
762
	});
763
764
	ImGui::ColorEdit4("Shade color", shadeColor.data(),
765
		ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_AlphaBar);
766
}
767
768
} // namespace openmsx