OXYGENHUB
◆ main openmsx / src / imgui / ImGuiSlotMap.cc
19.4 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "ImGuiSlotMap.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiMedia.hh"
6
#include "ImGuiUtils.hh"
7
8
#include "CartridgeSlotManager.hh"
9
#include "Debuggable.hh"
10
#include "Debugger.hh"
11
#include "DummyDevice.hh"
12
#include "MSXCPUInterface.hh"
13
#include "MSXDevice.hh"
14
#include "MSXMotherBoard.hh"
15
#include "MSXMultiMemDevice.hh"
16
#include "TclObject.hh"
17
18
#include "gl_vec.hh"
19
#include "static_vector.hh"
20
#include "strCat.hh"
21
#include "xrange.hh"
22
23
#include <imgui.h>
24
25
#include <algorithm>
26
#include <optional>
27
#include <ranges>
28
#include <string_view>
29
#include <utility>
30
#include <vector>
31
32
namespace openmsx {
33
34
static constexpr unsigned PAGE_SIZE = 0x4000;
35
static constexpr unsigned ADDRESS_SPACE = 4 * PAGE_SIZE;
36
37
void ImGuiSlotMap::save(ImGuiTextBuffer& buf)
38
{
39
	savePersistent(buf, *this, persistentElements);
40
}
41
42
void ImGuiSlotMap::loadLine(std::string_view name, zstring_view value)
43
{
44
	loadOnePersistent(name, value, *this, persistentElements);
45
}
46
47
// A continuous address range in one slot, covered by a single device.
48
// 'device' is nullptr for the parts where nothing is mapped.
49
struct Block {
50
	unsigned begin;
51
	unsigned end; // exclusive
52
	const MSXDevice* device;
53
};
54
55
// Draw 'text' centered in the rectangle, wrapped over as many lines as fit.
56
// The last line ends in an ellipsis if the whole text doesn't fit, and nothing
57
// is drawn at all when not even an ellipsis fits - the tooltip is what makes
58
// those cells readable.
59
static void drawText(ImDrawList* drawList, std::string_view text,
60
                     gl::vec2 min, gl::vec2 max, ImU32 color)
61
{
62
	const auto& style = ImGui::GetStyle();
63
	auto [width, height] = (max - min) - 2.0f * gl::vec2(style.CellPadding);
64
	auto lineHeight = ImGui::GetTextLineHeight();
65
	if ((height < lineHeight) || (width < ImGui::CalcTextSize("..."sv).x)) return;
66
67
	auto draw = [&](std::string_view line, float y) {
68
		auto lineWidth = ImGui::CalcTextSize(line).x;
69
		drawList->AddText(gl::vec2(min.x + 0.5f * ((max.x - min.x) - lineWidth), y),
70
		                  color, line.data(), line.data() + line.size());
71
	};
72
73
	// Wrap at word boundaries for as long as that works, then put what is left
74
	// on the last line, clipped. Wrapping stops for one of two reasons: there
75
	// is no room for another line, or the next word doesn't fit on a line of
76
	// its own. In the latter case CalcWordWrapPosition() would break inside the
77
	// word, which reads as garbage in a diagram. Both cases end the same way,
78
	// so a name is either fully wrapped or ends in an ellipsis.
79
	auto* font = ImGui::GetFont();
80
	auto fontSize = ImGui::GetFontSize();
81
	static_vector<std::string_view, 16> lines;
82
	auto maxLines = std::min(size_t(height / lineHeight), lines.max_size());
83
	std::string_view rest;
84
	const char* pos = text.data();
85
	const char* end = pos + text.size();
86
	while (pos < end) {
87
		auto remainder = std::string_view(pos, size_t(end - pos));
88
		if ((lines.size() + 1) == maxLines) { rest = remainder; break; }
89
		const char* stop = font->CalcWordWrapPosition(fontSize, pos, end, width);
90
		if ((stop == pos) ||                     // not even one word fits
91
		    ((stop != end) && (*stop != ' '))) { // would break inside a word
92
			rest = remainder;
93
			break;
94
		}
95
		lines.push_back(std::string_view(pos, size_t(stop - pos)));
96
		pos = stop;
97
		while ((pos < end) && (*pos == ' ')) ++pos; // eat the wrap point
98
	}
99
100
	auto numLines = lines.size() + (rest.empty() ? 0 : 1);
101
	auto y = min.y + 0.5f * ((max.y - min.y) - float(numLines) * lineHeight);
102
	for (auto line : lines) {
103
		draw(line, y);
104
		y += lineHeight;
105
	}
106
	// rightClip() adds the ellipsis, and returns 'rest' unchanged when it fits.
107
	if (!rest.empty()) draw(ImGui::rightClip(rest, width), y);
108
}
109
110
namespace {
111
// What the mouse is over, so that only one tooltip is shown even where blocks
112
// overlap after being enlarged to a hoverable size.
113
struct HoveredBlock {
114
	Block block{0, 0, nullptr};
115
	std::optional<unsigned> cartridgeSlot; // when it's in an external slot
116
	float height = 0.0f;
117
	bool valid = false;
118
};
119
120
// A block that is too small to see, enlarged and drawn on top of its
121
// neighbours instead of to scale.
122
struct TinyBlock {
123
	gl::vec2 min;
124
	gl::vec2 max;
125
	ImU32 color;
126
};
127
128
// Everything that only depends on the font and the style, so the same for the
129
// whole frame.
130
struct Metrics {
131
	float rowHeight;
132
	float headerHeight;
133
	float labelWidth;
134
	float minSlotWidth;
135
	float minMapHeight;
136
	gl::vec2 gap;
137
	float overhang;
138
};
139
140
struct DrawContext {
141
	ImGuiManager& manager;
142
	MSXCPUInterface& cpuInterface;
143
	const CartridgeSlotManager& slotManager;
144
	Debugger& debugger;
145
	const MSXDevice* dummyDevice;
146
	Metrics metrics;
147
	ImDrawList* drawList;
148
	ImU32 emptyColor;
149
	ImU32 occupiedColor;
150
	ImU32 externalColor;
151
	ImU32 outlineColor;
152
	ImU32 textColor;
153
	ImU32 dimTextColor;
154
	gl::vec2 mouse;
155
	HoveredBlock hovered;
156
	std::vector<std::pair<gl::vec2, gl::vec2>> outlines;   // one per primary slot
157
	std::vector<std::pair<gl::vec2, gl::vec2>> separators; // between the blocks
158
	std::vector<TinyBlock> tinyBlocks;
159
	std::vector<Block> blocks;
160
	std::vector<Block> scratch;
161
};
162
}
163
164
// Split the address space of one slot into blocks, in 'ctx.blocks'.
165
// MSXCPUInterface stores a device per 16kB page, and wraps devices that cover
166
// less than a page in an MSXMultiMemDevice, which knows the exact address
167
// ranges. Recombining both gives the real extent of every device, so that
168
// nothing here has to care about page boundaries.
169
// Note: 'ctx.blocks' and 'ctx.scratch' live in the DrawContext instead of being
170
//       local variables, so that the (up to) 16 calls in one frame share a
171
//       single buffer.
172
static void getBlocks(DrawContext& ctx, int ps, int ss)
173
{
174
	auto& scratch = ctx.scratch;
175
	scratch.clear();
176
	for (auto page : xrange(4)) {
177
		auto pageBegin = PAGE_SIZE * unsigned(page);
178
		const auto* device = ctx.cpuInterface.getMSXDevice(ps, ss, page);
179
		if (device == ctx.dummyDevice) continue;
180
		if (const auto* multi = dynamic_cast<const MSXMultiMemDevice*>(device)) {
181
			// Only these sub-ranges can be out of order: the pages themselves
182
			// are visited in increasing order, so sorting per page is enough.
183
			auto first = scratch.size();
184
			for (const auto& range : multi->getRanges()) {
185
				auto begin = std::max(range.base, pageBegin);
186
				auto end = std::min(range.base + range.size, pageBegin + PAGE_SIZE);
187
				if (begin < end) scratch.emplace_back(begin, end, range.device);
188
			}
189
			std::ranges::sort(scratch.begin() + first, scratch.end(), {}, &Block::begin);
190
		} else {
191
			scratch.emplace_back(pageBegin, pageBegin + PAGE_SIZE, device);
192
		}
193
	}
194
195
	// Glue the pieces of a device that spans page boundaries back together,
196
	// and fill what's left with 'empty' blocks.
197
	auto& result = ctx.blocks;
198
	result.clear();
199
	unsigned pos = 0;
200
	for (const auto& block : scratch) {
201
		if (!result.empty() && (result.back().device == block.device) &&
202
		    (result.back().end == block.begin)) {
203
			result.back().end = block.end;
204
		} else {
205
			if (pos < block.begin) result.emplace_back(pos, block.begin, nullptr);
206
			result.push_back(block);
207
		}
208
		pos = block.end;
209
	}
210
	if (pos < ADDRESS_SPACE) result.emplace_back(pos, ADDRESS_SPACE, nullptr);
211
}
212
213
static Metrics getMetrics()
214
{
215
	const auto& style = ImGui::GetStyle();
216
	auto lineHeight = ImGui::GetTextLineHeight();
217
	auto rowHeight = lineHeight + 2.0f * style.CellPadding.y;
218
	return {
219
		.rowHeight = rowHeight,
220
		.headerHeight = 2.0f * rowHeight, // external slot name, slot number
221
		// Wide enough for every label on the address axis. Letters and digits
222
		// don't have the same width, so check both kinds.
223
		.labelWidth = 2.0f * style.CellPadding.x +
224
		              std::max({ImGui::CalcTextSize("0000"sv).x,
225
		                        ImGui::CalcTextSize("C000"sv).x,
226
		                        ImGui::CalcTextSize("FFFF"sv).x}),
227
		// A primary slot always gets the same width, expanded or not, so
228
		// machines with a different slot layout still look alike.
229
		.minSlotWidth = 4.0f * 3.0f * lineHeight,
230
		.minMapHeight = 10.0f * rowHeight,
231
		.gap = 2.0f * gl::vec2(style.ItemSpacing),
232
		// The labels of the lowest and highest address stick out half a line.
233
		.overhang = 0.5f * rowHeight,
234
	};
235
}
236
237
// Below this a block can't be seen, let alone hovered. A single byte is a
238
// hundredth of a pixel on a 64kB axis, so those are drawn out of scale.
239
static constexpr float MIN_BLOCK_HEIGHT = 4.0f;
240
241
// One slot, drawn as a continuous 0x0000-0xFFFF axis with the highest address
242
// on top, so a device is a rectangle at its real position and size.
243
static void drawSlot(DrawContext& ctx, int ps, int ss, bool expanded,
244
                     float x0, float x1, float headerTop, float top, float bottom)
245
{
246
	// The slot number goes directly above the map, there are four times as many
247
	// of those as external slot names.
248
	auto cartridge = ctx.slotManager.findSlot(ps, ss, true);
249
	auto labelHeight = 0.5f * (top - headerTop);
250
	if (cartridge) {
251
		drawText(ctx.drawList, tmpStrCat("Slot ", char('A' + *cartridge)),
252
		         gl::vec2(x0, headerTop), gl::vec2(x1, headerTop + labelHeight),
253
		         ctx.dimTextColor);
254
	}
255
	drawText(ctx.drawList, expanded ? strCat(ps, '-', ss) : strCat(ps),
256
	         gl::vec2(x0, headerTop + labelHeight), gl::vec2(x1, top), ctx.textColor);
257
258
	auto height = bottom - top;
259
	auto addressToY = [&](unsigned address) {
260
		return bottom - height * (float(address) * (1.0f / float(ADDRESS_SPACE)));
261
	};
262
263
	getBlocks(ctx, ps, ss);
264
	// Blocks come in increasing address order, so downwards on screen. Keeps
265
	// enlarged blocks from covering each other.
266
	auto lowestEnlarged = bottom;
267
	for (const auto& block : ctx.blocks) {
268
		auto blockTop = addressToY(block.end);
269
		auto blockBottom = addressToY(block.begin);
270
		auto blockHeight = blockBottom - blockTop;
271
		auto color = !block.device ? ctx.emptyColor
272
		                           : (cartridge ? ctx.externalColor : ctx.occupiedColor);
273
274
		if (auto grow = 0.5f * (MIN_BLOCK_HEIGHT - blockHeight); grow > 0.0f) {
275
			// Too small to draw to scale, so draw it out of scale: enlarge it
276
			// around its real address, stack it on top of an earlier enlarged
277
			// block if they'd overlap, and paint it over its big neighbours
278
			// later on.
279
			blockTop -= grow;
280
			blockBottom += grow;
281
			if (blockBottom > lowestEnlarged) {
282
				blockTop -= blockBottom - lowestEnlarged;
283
				blockBottom = lowestEnlarged;
284
			}
285
			lowestEnlarged = blockTop;
286
			ctx.tinyBlocks.emplace_back(gl::vec2(x0, blockTop), gl::vec2(x1, blockBottom),
287
			                            color);
288
		} else {
289
			gl::vec2 min{x0, blockTop};
290
			gl::vec2 max{x1, blockBottom};
291
			ctx.drawList->AddRectFilled(min, max, color);
292
			// Only the line towards the next block: the outer edges belong to
293
			// the outline of the slot as a whole. Drawing a rectangle per block
294
			// would draw every shared edge twice. See the note on 'separators'
295
			// for why this stops one pixel short of x1.
296
			if (block.end != ADDRESS_SPACE) {
297
				ctx.separators.emplace_back(min, gl::vec2(x1 - 1.0f, blockTop));
298
			}
299
			drawText(ctx.drawList, block.device ? std::string_view(block.device->getName())
300
			                                    : "empty"sv,
301
			         min, max, ctx.textColor);
302
		}
303
304
		// Hit-test what is actually drawn. The smallest block wins, so an
305
		// enlarged one stays reachable between its much larger neighbours.
306
		if ((x0 <= ctx.mouse.x) && (ctx.mouse.x < x1) &&
307
		    (blockTop <= ctx.mouse.y) && (ctx.mouse.y < blockBottom) &&
308
		    (!ctx.hovered.valid || (blockHeight < ctx.hovered.height))) {
309
			ctx.hovered = HoveredBlock{block, cartridge, blockHeight, true};
310
		}
311
	}
312
}
313
314
static void drawDeviceToolTip(DrawContext& ctx)
315
{
316
	const auto& block = ctx.hovered.block;
317
	im::Tooltip([&]{
318
		im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&]{
319
			bool header = false;
320
			// For an external slot, what is plugged into it. That's not the
321
			// same thing as the device below, which is a part of it, though
322
			// for a simple extension the two names are the same.
323
			if (auto slot = ctx.hovered.cartridgeSlot) {
324
				auto content = ctx.manager.media->displayNameForSlotContent(
325
					ctx.slotManager, *slot);
326
				if (block.device && (content == block.device->getName())) {
327
					ImGui::StrCat("Slot ", char('A' + *slot));
328
				} else {
329
					ImGui::StrCat("Slot ", char('A' + *slot), ": ", content);
330
				}
331
				header = true;
332
			}
333
			if (block.device) {
334
				ImGui::TextUnformatted(block.device->getName());
335
				header = true;
336
			}
337
			if (header) ImGui::Separator();
338
			// Below 1kB the size in kB would round down to 0.
339
			auto printSize = [](std::string_view label, unsigned bytes) {
340
				auto unit = (bytes == 1) ? " byte"sv : " bytes"sv;
341
				ImGui::StrCat(label, ": ", (bytes < 1024) ? strCat(bytes, unit)
342
				                                          : strCat(bytes / 1024, "kB"));
343
			};
344
			ImGui::StrCat("address: 0x", hex_string<4, HexCase::upper>(block.begin),
345
			              " - 0x", hex_string<4, HexCase::upper>(block.end - 1));
346
			printSize("size", block.end - block.begin);
347
			if (!block.device) return;
348
349
			// Whatever the device wants to tell about itself, e.g. the mapper
350
			// type of a ROM or the file it was loaded from. Ask the device
351
			// directly instead of going through the 'machine_info device'
352
			// command: we already have the MSXDevice here.
353
			TclObject info;
354
			block.device->getDeviceInfo(info);
355
			for (size_t i = 0; (i + 1) < info.size(); i += 2) {
356
				auto key = info.getListIndexUnchecked(i).getString();
357
				auto value = info.getListIndexUnchecked(i + 1).getString();
358
				if (value.empty()) continue;
359
				ImGui::StrCat(key, ": ", value);
360
			}
361
			// Devices with a debuggable of the same name (memory mappers, RAM)
362
			// can also tell how much memory they actually have.
363
			if (const auto* debuggable = ctx.debugger.findDebuggable(block.device->getName())) {
364
				if (auto bytes = debuggable->getSize(); bytes != 0) {
365
					printSize("total memory", bytes);
366
				}
367
			}
368
		});
369
	});
370
}
371
372
static void drawSlotMap(DrawContext& ctx)
373
{
374
	auto [rowHeight, headerHeight, labelWidth, minSlotWidth, minMapHeight, gap, overhang] =
375
		ctx.metrics;
376
	auto avail = gl::vec2(ImGui::GetContentRegionAvail());
377
378
	// Reflow: put 4, 2 or 1 primary slots next to each other, whichever fits.
379
	auto widthFor = [&](int perRow) {
380
		return labelWidth + float(perRow) * minSlotWidth + float(perRow - 1) * gap.x;
381
	};
382
	int perRow = 4;
383
	if (widthFor(perRow) > avail.x) perRow = 2;
384
	if ((perRow == 2) && (widthFor(perRow) > avail.x)) perRow = 1;
385
	int numRows = 4 / perRow;
386
387
	auto slotWidth = std::max(minSlotWidth,
388
		(avail.x - labelWidth - float(perRow - 1) * gap.x) / float(perRow));
389
	auto mapHeight = std::max(minMapHeight,
390
		(avail.y - overhang - float(numRows) * headerHeight - float(numRows - 1) * gap.y) /
391
		float(numRows));
392
393
	auto origin = gl::vec2(ImGui::GetCursorScreenPos());
394
	gl::vec2 size{labelWidth + float(perRow) * slotWidth + float(perRow - 1) * gap.x,
395
	              float(numRows) * (headerHeight + mapHeight) + float(numRows - 1) * gap.y +
396
	              overhang};
397
	ImGui::Dummy(size); // reserve the space, the map itself is positioned absolutely
398
399
	for (auto row : xrange(numRows)) {
400
		auto headerTop = origin.y + float(row) * (headerHeight + mapHeight + gap.y);
401
		auto top = headerTop + headerHeight;
402
		auto bottom = top + mapHeight;
403
		auto addressToY = [&](unsigned address) {
404
			return bottom - mapHeight * (float(address) * (1.0f / float(ADDRESS_SPACE)));
405
		};
406
407
		// Address axis: one label per page boundary, plus the very top, each
408
		// centered on the position it marks.
409
		auto drawTick = [&](unsigned address, std::string_view label) {
410
			auto y = addressToY(address);
411
			drawText(ctx.drawList, label,
412
			         gl::vec2(origin.x, y - 0.5f * rowHeight),
413
			         gl::vec2(origin.x + labelWidth, y + 0.5f * rowHeight),
414
			         ctx.textColor);
415
		};
416
		for (auto page : xrange(4)) {
417
			auto address = PAGE_SIZE * unsigned(page);
418
			drawTick(address, tmpStrCat(hex_string<4, HexCase::upper>(address)));
419
		}
420
		drawTick(ADDRESS_SPACE, "FFFF"sv);
421
422
		auto x = origin.x + labelWidth;
423
		for (auto ps : xrange(row * perRow, std::min((row + 1) * perRow, 4))) {
424
			if (ctx.cpuInterface.isExpanded(ps)) {
425
				auto width = 0.25f * slotWidth;
426
				for (auto ss : xrange(4)) {
427
					auto x0 = x + float(ss) * width;
428
					drawSlot(ctx, ps, ss, true, x0, x0 + width, headerTop, top, bottom);
429
					// Between the sub-slots only, the outer edges are part
430
					// of the outline of the primary slot.
431
					if (ss != 0) {
432
						ctx.separators.emplace_back(gl::vec2(x0, top),
433
						                            gl::vec2(x0, bottom - 1.0f));
434
					}
435
				}
436
			} else {
437
				drawSlot(ctx, ps, 0, false, x, x + slotWidth, headerTop, top, bottom);
438
			}
439
			ctx.outlines.emplace_back(gl::vec2(x, top), gl::vec2(x + slotWidth, bottom));
440
			x += slotWidth + gap.x;
441
		}
442
	}
443
444
	// Outlines in one go, after all the fills, so that the fill of one block
445
	// can't paint over the outline of the block next to it. This is also what
446
	// keeps a device that is too small to see as a rectangle visible as a line.
447
	// Each edge is drawn exactly once, otherwise the shared ones would come out
448
	// twice as thick as the rest.
449
	// Note: AddRect() treats 'max' as exclusive (it strokes half a pixel inside
450
	//       it, like AddRectFilled() fills up to just before it), while both end
451
	//       points of AddLine() are inclusive. So a separator has to stop one
452
	//       pixel short to end where the outline around it is, see drawSlot().
453
	for (const auto& [p1, p2] : ctx.separators) {
454
		ctx.drawList->AddLine(p1, p2, ctx.outlineColor);
455
	}
456
	ctx.separators.clear();
457
	for (const auto& [min, max] : ctx.outlines) {
458
		ctx.drawList->AddRect(min, max, ctx.outlineColor);
459
	}
460
	ctx.outlines.clear();
461
462
	// Devices too small to draw to scale go last, so that they end up visible
463
	// on top of the (much bigger) devices around them.
464
	for (const auto& [min, max, color] : ctx.tinyBlocks) {
465
		ctx.drawList->AddRectFilled(min, max, color);
466
		ctx.drawList->AddRect(min, max, ctx.outlineColor);
467
	}
468
	ctx.tinyBlocks.clear();
469
}
470
471
// Big enough for a 2x2 layout: room for three primary slots next to each other,
472
// while the reflow needs room for four before it puts them in a single row.
473
static gl::vec2 defaultWindowSize(const Metrics& m)
474
{
475
	const auto& style = ImGui::GetStyle();
476
	gl::vec2 content{m.labelWidth + 3.0f * (m.minSlotWidth + m.gap.x),
477
	                 2.0f * (m.headerHeight + m.minMapHeight) + m.gap.y + m.overhang};
478
	return content + 2.0f * gl::vec2(style.WindowPadding) +
479
	       gl::vec2(0.0f, ImGui::GetFrameHeight()); // title bar
480
}
481
482
void ImGuiSlotMap::paint(MSXMotherBoard* motherBoard)
483
{
484
	if (!show) return;
485
	if (!motherBoard) return;
486
487
	auto& cpuInterface = motherBoard->getCPUInterface();
488
	DrawContext ctx{
489
		.manager = manager,
490
		.cpuInterface = cpuInterface,
491
		.slotManager = motherBoard->getSlotManager(),
492
		.debugger = motherBoard->getDebugger(),
493
		.dummyDevice = &cpuInterface.getDummyDevice(),
494
		.metrics = getMetrics(),
495
		.drawList = nullptr, // only valid inside the window
496
		.emptyColor = ImGui::GetColorU32(getColor(imColor::GRAY), 0.4f),
497
		.occupiedColor = ImGui::GetColorU32(ImGuiCol_Header),
498
		.externalColor = getColor(imColor::YELLOW_BG),
499
		.outlineColor = ImGui::GetColorU32(ImGuiCol_Text),
500
		.textColor = ImGui::GetColorU32(ImGuiCol_Text),
501
		.dimTextColor = ImGui::GetColorU32(ImGuiCol_TextDisabled),
502
		.mouse = gl::vec2(ImGui::GetIO().MousePos),
503
		// Default initialization is what we want for the rest, but spell it out
504
		// to keep gcc's -Wmissing-field-initializers quiet.
505
		.hovered = {},
506
		.outlines = {},
507
		.separators = {},
508
		.tinyBlocks = {},
509
		.blocks = {},
510
		.scratch = {},
511
	};
512
	ctx.outlines.reserve(4);
513
	ctx.separators.reserve(64);
514
515
	ImGui::SetNextWindowSize(defaultWindowSize(ctx.metrics), ImGuiCond_FirstUseEver);
516
	im::Window("Slot map", &show, [&]{
517
		ctx.drawList = ImGui::GetWindowDrawList();
518
		drawSlotMap(ctx);
519
520
		if (ctx.hovered.valid && ImGui::IsWindowHovered()) {
521
			drawDeviceToolTip(ctx);
522
		}
523
	});
524
}
525
526
} // namespace openmsx