OXYGENHUB
◆ main openmsx / src / imgui / ImGuiIoPorts.cc
9.6 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiIoPorts.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiUtils.hh"
6
7
#include "DummyDevice.hh"
8
#include "EmuTime.hh"
9
#include "HardwareConfig.hh"
10
#include "MSXCPUInterface.hh"
11
#include "MSXDevice.hh"
12
#include "MSXMotherBoard.hh"
13
#include "MSXMultiIODevice.hh"
14
15
#include "join.hh"
16
#include "narrow.hh"
17
#include "strCat.hh"
18
#include "xrange.hh"
19
20
#include <algorithm>
21
#include <array>
22
#include <cstdint>
23
#include <ranges>
24
#include <string>
25
#include <string_view>
26
#include <vector>
27
28
namespace openmsx {
29
30
using namespace std::literals;
31
32
/** A maximal range of ports with the same device(s) in the same direction(s).
33
  */
34
struct Row {
35
	uint8_t begin; // inclusive
36
	uint8_t end;   // inclusive
37
	bool in;
38
	bool out;
39
	bool overlap;       // these ports are shared with another device
40
	std::string device; // comma separated name(s)
41
};
42
43
using Devices = MSXMultiIODevice::Devices;
44
45
// Collect the device(s) on one port. This mirrors 'machine_info
46
// input_port/output_port', on which the 'iomap' script is built: an unused
47
// port yields an empty list, a port with conflicting devices one entry per
48
// device.
49
[[nodiscard]] static Devices getDevices(MSXCPUInterface& cpuInterface, uint8_t port, bool isIn)
50
{
51
	auto* device = cpuInterface.getIODevice(port, isIn);
52
	const MSXDevice* dummy = &cpuInterface.getDummyDevice();
53
	if (device == dummy) return {}; // unused port
54
	if (auto* multi = dynamic_cast<MSXMultiIODevice*>(device)) {
55
		// A sub-device is never the DummyDevice: register_IO() only wraps
56
		// devices in a MSXMultiIODevice once a real device is registered,
57
		// and unregister_IO() unwraps again when one device is left.
58
		return multi->getDevices();
59
	}
60
	return {device};
61
}
62
63
struct PortDevices {
64
	std::array<Devices, 256> in;
65
	std::array<Devices, 256> out;
66
};
67
[[nodiscard]] static PortDevices collectDevices(MSXCPUInterface& cpuInterface)
68
{
69
	PortDevices result;
70
	for (auto port : xrange(256)) {
71
		result.in [port] = getDevices(cpuInterface, narrow<uint8_t>(port), true);
72
		result.out[port] = getDevices(cpuInterface, narrow<uint8_t>(port), false);
73
	}
74
	return result;
75
}
76
77
[[nodiscard]] static std::string portsText(const Row& row)
78
{
79
	if (row.begin == row.end) {
80
		return strCat(hex_string<2, HexCase::upper>(row.begin));
81
	}
82
	return strCat(hex_string<2, HexCase::upper>(row.begin), '-',
83
	              hex_string<2, HexCase::upper>(row.end));
84
}
85
86
[[nodiscard]] static std::string_view dirText(const Row& row)
87
{
88
	return row.in ? (row.out ? "I/O"sv : "I"sv) : "O"sv;
89
}
90
91
static void sortRows(std::vector<Row>& rows)
92
{
93
	// Note: unlike the other views in openMSX, 'rows' is rebuilt from scratch
94
	// every frame. So (re)apply the sort unconditionally instead of only when
95
	// 'SpecsDirty' is set.
96
	const auto* sortSpecs = ImGui::TableGetSortSpecs();
97
	if (!sortSpecs || (sortSpecs->SpecsCount == 0)) return;
98
	assert(sortSpecs->Specs);
99
100
	bool descending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Descending;
101
	switch (sortSpecs->Specs->ColumnIndex) {
102
	case 0: // ports: 'rows' is already generated in ascending port order
103
		if (descending) std::ranges::reverse(rows);
104
		break;
105
	case 2: // device: stable, so each device keeps its ports port-ordered
106
		sortUpDown_String(rows, sortSpecs, &Row::device);
107
		break;
108
	default:
109
		// 'Dir' and 'Value' are NoSort, but Dear ImGui still restores a sort
110
		// on such a column from imgui.ini (TableSortSpecsSanitize() only
111
		// clears it for hidden columns). So don't assume this can't happen,
112
		// just keep the rows in ascending port order.
113
		break;
114
	}
115
}
116
117
[[nodiscard]] static std::vector<Row> getRows(MSXCPUInterface& cpuInterface)
118
{
119
	auto devices = collectDevices(cpuInterface);
120
	const auto& in = devices.in;
121
	const auto& out = devices.out;
122
123
	std::vector<Row> result;
124
	unsigned port = 0;
125
	while (port < 256) {
126
		// coalesce consecutive ports with identical input and output device(s)
127
		unsigned end = port + 1;
128
		while ((end < 256) && (in[end] == in[port]) && (out[end] == out[port])) {
129
			++end;
130
		}
131
		auto add = [&](const Devices& devs, bool isIn, bool isOut) {
132
			if (devs.empty()) return;
133
			result.push_back(Row{
134
				.begin = narrow<uint8_t>(port),
135
				.end = narrow<uint8_t>(end - 1),
136
				.in = isIn,
137
				.out = isOut,
138
				.overlap = devs.size() > 1,
139
				.device = join(std::views::transform(devs, &MSXDevice::getName), ", ")});
140
		};
141
		if (in[port] == out[port]) {
142
			add(in[port], true, true);
143
		} else {
144
			add(in [port], true, false);
145
			add(out[port], false, true);
146
		}
147
		port = end;
148
	}
149
	return result;
150
}
151
152
static void drawValue(ImFont* fontMono, MSXCPUInterface& cpuInterface,
153
                      const Row& row, EmuTime time)
154
{
155
	im::ScopedFont sf(fontMono);
156
	if (!row.in) {
157
		// Peeking would read the input side, which has nothing to do with
158
		// the (output-only) device on this row.
159
		ImGui::TextUnformatted("-"sv);
160
		return;
161
	}
162
	// Draw all values and let Dear ImGui clip them. Only the gfx9000 has more
163
	// than a handful of ports, and this column is hidden by default anyway.
164
	std::string values;
165
	for (auto port : xrange(unsigned(row.begin), unsigned(row.end) + 1)) {
166
		if (!values.empty()) strAppend(values, ' ');
167
		strAppend(values, hex_string<2, HexCase::upper>(
168
			cpuInterface.peekIO(narrow<uint16_t>(port), time)));
169
	}
170
	ImGui::TextUnformatted(values);
171
}
172
173
static void drawTable(ImFont* fontMono, MSXCPUInterface& cpuInterface,
174
                      bool warnOverlap, EmuTime time)
175
{
176
	// 'Ports' and 'Dir' have a fixed width, so it must fit both the content
177
	// and the header. 'Ports' is sortable, and a sorted column also draws a
178
	// sort arrow next to its name, otherwise that header would be clipped to
179
	// "..." as soon as you sort on it.
180
	const auto& style = ImGui::GetStyle();
181
	auto textWidth = [](std::string_view s) { return ImGui::CalcTextSize(s).x; };
182
	auto monoWidth = [&](std::string_view s) {
183
		im::ScopedFont sf(fontMono);
184
		return ImGui::CalcTextSize(s).x;
185
	};
186
	auto columnWidth = [&](float header, float content) {
187
		return std::max(header, content) + 2.0f * style.CellPadding.x;
188
	};
189
	auto arrowWidth = ImGui::GetFontSize() + style.ItemInnerSpacing.x;
190
191
	static constexpr int TABLE_FLAGS =
192
		ImGuiTableFlags_RowBg |
193
		ImGuiTableFlags_BordersInnerV |
194
		ImGuiTableFlags_Resizable |
195
		ImGuiTableFlags_Reorderable |
196
		ImGuiTableFlags_Hideable |
197
		ImGuiTableFlags_ContextMenuInBody |
198
		ImGuiTableFlags_Sortable;
199
	im::Table("ports", 4, TABLE_FLAGS, [&]{
200
		// 'Value' is hidden by default: the hex editor on the 'ioports'
201
		// debuggable already shows these values. Un-hide it via the
202
		// right-click context menu on the table header.
203
		// It's also not sortable: the values change while the emulation
204
		// runs, so sorting on them would reshuffle the rows every frame.
205
		//
206
		// 'Ports' and 'Dir' hold at most 5 resp. 3 characters, so resizing
207
		// them can only take space away from the other two. Those stretch
208
		// instead, so widening the window widens both.
209
		ImGui::TableSetupColumn("Ports", ImGuiTableColumnFlags_WidthFixed |
210
		                                 ImGuiTableColumnFlags_NoResize |
211
		                                 ImGuiTableColumnFlags_DefaultSort,
212
		                        columnWidth(textWidth("Ports"sv) + arrowWidth,
213
		                                    monoWidth("FF-FF"sv)));
214
		ImGui::TableSetupColumn("Dir", ImGuiTableColumnFlags_WidthFixed |
215
		                               ImGuiTableColumnFlags_NoResize |
216
		                               ImGuiTableColumnFlags_NoSort,
217
		                        columnWidth(textWidth("Dir"sv), textWidth("I/O"sv)));
218
		ImGui::TableSetupColumn("Device", ImGuiTableColumnFlags_WidthStretch);
219
		ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch |
220
		                                 ImGuiTableColumnFlags_DefaultHide |
221
		                                 ImGuiTableColumnFlags_NoSort);
222
		ImGui::TableHeadersRow();
223
224
		auto rows = getRows(cpuInterface);
225
		sortRows(rows);
226
		for (const auto& row : rows) {
227
			if (ImGui::TableNextColumn()) { // ports
228
				im::ScopedFont sf(fontMono);
229
				ImGui::TextUnformatted(portsText(row));
230
			}
231
			if (ImGui::TableNextColumn()) { // direction
232
				ImGui::TextUnformatted(dirText(row));
233
			}
234
			if (ImGui::TableNextColumn()) { // device(s)
235
				bool conflict = row.overlap && warnOverlap;
236
				im::StyleColor(conflict, ImGuiCol_Text, getColor(imColor::YELLOW), [&]{
237
					ImGui::TextUnformatted(row.device);
238
				});
239
				if (conflict) {
240
					simpleToolTip(
241
						"These ports are shared with another device. Reading "
242
						"such a port returns the bitwise AND of all values, "
243
						"writing goes to every device."sv);
244
				}
245
			}
246
			if (ImGui::TableNextColumn()) { // value(s)
247
				drawValue(fontMono, cpuInterface, row, time);
248
			}
249
		}
250
	});
251
}
252
253
void ImGuiIoPorts::save(ImGuiTextBuffer& buf)
254
{
255
	savePersistent(buf, *this, persistentElements);
256
}
257
258
void ImGuiIoPorts::loadLine(std::string_view name, zstring_view value)
259
{
260
	loadOnePersistent(name, value, *this, persistentElements);
261
}
262
263
void ImGuiIoPorts::paint(MSXMotherBoard* motherBoard)
264
{
265
	if (!show || !motherBoard) return;
266
267
	// Machines can explicitly declare that overlapping I/O ports are
268
	// intentional, in that case don't flag them.
269
	const auto* config = motherBoard->getMachineConfig();
270
	bool warnOverlap = !config ||
271
		config->getDevicesElem().getAttributeValueAsBool("overlap_warning", true);
272
273
	auto fontSize = ImGui::GetFontSize();
274
	ImGui::SetNextWindowSize(ImVec2(46.0f * fontSize, 26.0f * fontSize), ImGuiCond_FirstUseEver);
275
	im::Window("I/O ports", &show, [&]{
276
		// A device doesn't necessarily occupy a contiguous range of ports
277
		// (e.g. the Philips NMS-1205). Sort on 'Device' to see all ports of
278
		// one device together instead of ordered by port number.
279
		drawTable(manager.fontMono, motherBoard->getCPUInterface(),
280
		          warnOverlap, motherBoard->getCurrentTime());
281
	});
282
}
283
284
} // namespace openmsx