OXYGENHUB
◆ main openmsx / src / imgui / ImGuiBitmapViewer.cc
23.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "ImGuiBitmapViewer.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiPalette.hh"
6
#include "ImGuiUtils.hh"
7
8
#include "DisplayMode.hh"
9
#include "VDP.hh"
10
#include "VDPVRAM.hh"
11
12
#include "MemBuffer.hh"
13
14
#include <imgui.h>
15
16
namespace openmsx {
17
18
using namespace std::literals;
19
using Point = VDPCmdEngine::Point;
20
using Rect = VDPCmdEngine::Rect;
21
22
// Given a VDPCmdEngine-rectangle and a point, split that rectangle in a 'done'
23
// and a 'todo' part.
24
//
25
// The VDPCmdEngine walks over this rectangle line by line. But the direction
26
// can still vary ('dix' and 'diy'). These directions are implicit: the first
27
// 'Point' in 'Rect' is the start point, the second is the end point. (Start and
28
// end points are inclusive).
29
//
30
// The given point is assumed to be 'done' (in reality we can make a small error
31
// here, but that's just a few dozen VDP cycles, so just a few Z80
32
// instructions). If the point lies outside the rectangle, the result will be
33
// shown as fully 'done'.
34
//
35
// The full result contains 1 or 2 'done' parts, and 0, 1 or 2 'todo' parts. So
36
// e.g. a top rectangle of fully processed lines, a middle line that's in
37
// progress (split over two lines (represented as a rectangle with height=1)),
38
// and a bottom rectangle of still todo lines.
39
//
40
// All the sub-rectangles in the result have their corners in 'natural' order:
41
// left-to-right and top-to-bottom (this simplifies drawing later). This is
42
// explicitly not the case for the input rectangle(s) (those are ordered
43
// according to 'dix' and 'diy').
44
//
45
// The order of the sub-rectangles in the output is no longer guaranteed to be
46
// in VDP command processing order, and for drawing the overlay that doesn't
47
// matter.
48
// TODO unittest
49
struct DoneTodo {
50
	static_vector<Rect, 2> done, todo;
51
};
52
[[nodiscard]] static DoneTodo splitRect(const Rect& r, int x, int y)
53
{
54
	DoneTodo result;
55
	auto& done = result.done;
56
	auto& todo = result.todo;
57
58
	auto minX = std::min(r.p1.x, r.p2.x);
59
	auto maxX = std::max(r.p1.x, r.p2.x);
60
	auto minY = std::min(r.p1.y, r.p2.y);
61
	auto maxY = std::max(r.p1.y, r.p2.y);
62
63
	if ((x < minX) || (x > maxX) || (y < minY) || (y > maxY)) {
64
		// outside rect
65
		done.emplace_back(Point{minX, minY}, Point{maxX, maxY});
66
		return result;
67
	}
68
69
	bool diy = r.p1.y > r.p2.y;
70
	if (minY < y) {
71
		// top part (either done or todo), full lines
72
		auto& res = diy ? todo : done;
73
		res.emplace_back(Point{minX, minY}, Point{maxX, y - 1});
74
	}
75
	if (r.p1.x <= r.p2.x) {
76
		// left-to-right
77
		assert(r.p1.x <= x);
78
		done.emplace_back(Point{r.p1.x, y}, Point{x, y});
79
		if (x < r.p2.x) {
80
			todo.emplace_back(Point{x + 1, y}, Point{r.p2.x, y});
81
		}
82
	} else {
83
		// right to-left
84
		assert(x <= r.p1.x);
85
		done.emplace_back(Point{x, y}, Point{r.p1.x, y});
86
		if (r.p2.x < x) {
87
			todo.emplace_back(Point{r.p2.x, y}, Point{x - 1, y});
88
		}
89
	}
90
91
	if (y < maxY) {
92
		// bottom part (either done or todo), full lines
93
		auto& res = diy ? done : todo;
94
		res.emplace_back(Point{minX, y + 1}, Point{maxX, maxY});
95
	}
96
97
	return result;
98
}
99
100
ImGuiBitmapViewer::ImGuiBitmapViewer(ImGuiManager& manager_, size_t index)
101
	: ImGuiPart(manager_)
102
	, title(strCat("Bitmap viewer", strCat_if(index, " (", index + 1, ')')))
103
{
104
}
105
106
void ImGuiBitmapViewer::save(ImGuiTextBuffer& buf)
107
{
108
	savePersistent(buf, *this, persistentElements);
109
}
110
111
void ImGuiBitmapViewer::loadLine(std::string_view name, zstring_view value)
112
{
113
	loadOnePersistent(name, value, *this, persistentElements);
114
}
115
116
void ImGuiBitmapViewer::paint(MSXMotherBoard* motherBoard)
117
{
118
	if (!show) return;
119
	if (!motherBoard) return;
120
121
	ImGui::SetNextWindowSize({528, 620}, ImGuiCond_FirstUseEver);
122
	im::Window(title.c_str(), &show, [&]{
123
		auto* vdp = dynamic_cast<VDP*>(motherBoard->findDevice("VDP")); // TODO name based OK?
124
		if (!vdp || vdp->isMSX1VDP()) return;
125
126
		auto parseMode = [](DisplayMode mode) {
127
			auto base = mode.getBase();
128
			if (base == DisplayMode::GRAPHIC4) return SCR5;
129
			if (base == DisplayMode::GRAPHIC5) return SCR6;
130
			if (base == DisplayMode::GRAPHIC6) return SCR7;
131
			if (base != DisplayMode::GRAPHIC7) return OTHER;
132
			if (mode.getByte() & DisplayMode::YJK) {
133
				if (mode.getByte() & DisplayMode::YAE) {
134
					return SCR11;
135
				} else {
136
					return SCR12;
137
				}
138
			} else {
139
				return SCR8;
140
			}
141
		};
142
		int vdpMode = parseMode(vdp->getDisplayMode());
143
144
		int vdpPages = vdpMode <= SCR6 ? 4 : 2;
145
		int vdpPage = vdp->getDisplayPage();
146
		if (vdpPage >= vdpPages) vdpPage &= 1;
147
148
		int vdpLines = (vdp->getNumberOfLines() == 192) ? 0 : 1;
149
150
		int vdpColor0 = [&]{
151
			if (vdpMode == one_of(SCR8, SCR11, SCR12) || !vdp->getTransparency()) {
152
				return 16; // no replacement
153
			}
154
			return vdp->getBackgroundColor() & 15;
155
		}();
156
157
		auto modeToStr = [](int mode) {
158
			if (mode == SCR5 ) return "screen 5";
159
			if (mode == SCR6 ) return "screen 6";
160
			if (mode == SCR7 ) return "screen 7";
161
			if (mode == SCR8 ) return "screen 8";
162
			if (mode == SCR11) return "screen 11";
163
			if (mode == SCR12) return "screen 12";
164
			if (mode == OTHER) return "non-bitmap";
165
			assert(false); return "ERROR";
166
		};
167
168
		static const char* const color0Str = "0\0001\0002\0003\0004\0005\0006\0007\0008\0009\00010\00011\00012\00013\00014\00015\000none\000";
169
		bool manualMode   = overrideAll || overrideMode;
170
		bool manualPage   = overrideAll || overridePage;
171
		bool manualLines  = overrideAll || overrideLines;
172
		bool manualColor0 = overrideAll || overrideColor0;
173
		im::Group([&]{
174
			ImGui::AlignTextToFramePadding();
175
			ImGui::TextUnformatted("VDP settings");
176
			auto pos = ImGui::GetCursorPos();
177
			ImGui::Dummy(ImGui::CalcTextSize("Screen mode: non-bitmap"));
178
			ImGui::SetCursorPos(pos);
179
			im::Disabled(manualMode, [&]{
180
				ImGui::AlignTextToFramePadding();
181
				ImGui::StrCat("Screen mode: ", modeToStr(vdpMode));
182
			});
183
			im::Disabled(manualPage, [&]{
184
				ImGui::AlignTextToFramePadding();
185
				ImGui::StrCat("Display page: ", vdpPage);
186
			});
187
			im::Disabled(manualLines, [&]{
188
				ImGui::AlignTextToFramePadding();
189
				ImGui::StrCat("Visible lines: ", vdpLines ? 212 : 192);
190
			});
191
			im::Disabled(manualColor0, [&]{
192
				ImGui::AlignTextToFramePadding();
193
				ImGui::StrCat("Replace color 0: ", getComboString(vdpColor0, color0Str));
194
			});
195
			// TODO interlace
196
		});
197
		ImGui::SameLine();
198
		im::Group([&]{
199
			ImGui::Checkbox("Manual override", &overrideAll);
200
			im::Group([&]{
201
				im::Disabled(overrideAll, [&]{
202
					ImGui::Checkbox("##mode",   overrideAll ? &overrideAll : &overrideMode);
203
					ImGui::Checkbox("##page",   overrideAll ? &overrideAll : &overridePage);
204
					ImGui::Checkbox("##lines",  overrideAll ? &overrideAll : &overrideLines);
205
					ImGui::Checkbox("##color0", overrideAll ? &overrideAll : &overrideColor0);
206
				});
207
			});
208
			ImGui::SameLine();
209
			im::Group([&]{
210
				im::ItemWidth(ImGui::GetFontSize() * 9.0f, [&]{
211
					im::Disabled(!manualMode, [&]{
212
						ImGui::Combo("##Screen mode", &bitmapScrnMode, "screen 5\000screen 6\000screen 7\000screen 8\000screen 11\000screen 12\000");
213
					});
214
					im::Disabled(!manualPage, [&]{
215
						// note: 'mode', not 'bitmapScrnMode'
216
						int mode = manualMode ? bitmapScrnMode : vdpMode;
217
						int numPages = mode <= SCR6 ? 4 : 2; // TODO extended VRAM
218
						if (bitmapPage >= numPages) bitmapPage = numPages - 1;
219
						if (bitmapPage < 0) bitmapPage = numPages;
220
						ImGui::Combo("##Display page", &bitmapPage, numPages == 2 ? "0\0001\000All\000" : "0\0001\0002\0003\000All\000");
221
						if (bitmapPage == numPages) bitmapPage = -1;
222
					});
223
					im::Disabled(!manualLines || bitmapPage < 0, [&]{
224
						ImGui::Combo("##Visible lines", &bitmapLines, "192\000212\000256\000");
225
					});
226
					im::Disabled(!manualColor0, [&]{
227
						ImGui::Combo("##Color 0 replacement", &bitmapColor0, color0Str);
228
					});
229
				});
230
			});
231
		});
232
233
		ImGui::SameLine();
234
		ImGui::Dummy(ImVec2(15, 1));
235
		ImGui::SameLine();
236
237
		const auto& vram = vdp->getVRAM();
238
		int mode   = manualMode   ? bitmapScrnMode : vdpMode;
239
		int page   = manualPage   ? bitmapPage     : vdpPage;
240
		int lines  = manualLines  ? bitmapLines    : vdpLines;
241
		int color0 = manualColor0 ? bitmapColor0   : vdpColor0;
242
		int divX = mode == one_of(SCR6, SCR7) ? 1 : 2;
243
		int width  = 512 / divX;
244
		int height = (lines == 0) ? 192
245
		           : (lines == 1) ? 212
246
		           : 256;
247
		if (page < 0) {
248
			int numPages = mode <= SCR6 ? 4 : 2;
249
			height = 256 * numPages;
250
			page = 0;
251
		}
252
		auto rasterBeamPos = vdp->getMSXPos(vdp->getCurrentTime());
253
		rasterBeamPos.x /= divX;
254
255
		im::Group([&]{
256
			ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10.0f);
257
			ImGui::Combo("Palette", &manager.palette->whichPalette, "VDP\000Custom\000Fixed\000");
258
			if (ImGui::Button("Open palette editor")) {
259
				manager.palette->window.raise();
260
			}
261
			ImGui::Separator();
262
			ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.0f);
263
			ImGui::Combo("Zoom", &bitmapZoom, "1x\0002x\0003x\0004x\0005x\0006x\0007x\0008x\000");
264
			ImGui::Checkbox("grid", &bitmapGrid);
265
			ImGui::SameLine();
266
			im::Disabled(!bitmapGrid, [&]{
267
				ImGui::ColorEdit4("Grid color", bitmapGridColor.data(),
268
					ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
269
			});
270
			ImGui::Separator();
271
			ImGui::Checkbox("beam", &rasterBeam);
272
			ImGui::SameLine();
273
			im::Disabled(!rasterBeam, [&]{
274
				ImGui::ColorEdit4("raster beam color", rasterBeamColor.data(),
275
					ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
276
				ImGui::SameLine();
277
				im::Font(manager.fontMono, [&]{
278
					ImGui::StrCat('(',  dec_string<4>(rasterBeamPos.x),
279
					              ',', dec_string<4>(rasterBeamPos.y), ')');
280
				});
281
			});
282
			HelpMarker("Position of the raster beam, expressed in MSX coordinates.\n"
283
			           "Left/top border have negative x/y-coordinates.\n"
284
			           "Only practically useful when emulation is paused.");
285
		});
286
287
		// VDP block command overlay settings
288
		auto& cmdEngine = vdp->getCmdEngine();
289
		bool inProgress = cmdEngine.commandInProgress(motherBoard->getCurrentTime());
290
291
		auto regs = cmdEngine.getLastCommand();
292
		auto formatted = VDPCmdEngine::formatCommand(regs, mode);
293
294
		im::TreeNode("Show VDP block command overlay", [&]{
295
			ImGui::RadioButton("never", &showCmdOverlay, 0);
296
			HelpMarker("Don't draw any overlay in the bitmap view");
297
			ImGui::SameLine();
298
			ImGui::RadioButton("in progress", &showCmdOverlay, 1);
299
			HelpMarker("Only show overlay for VDP block command that is currently executing");
300
			ImGui::SameLine();
301
			ImGui::RadioButton("also finished", &showCmdOverlay, 2);
302
			HelpMarker("Show overlay for the currently executing VDP block command, but also the last finished block command");
303
304
			im::TreeNode("Overlay colors", [&]{
305
				int colorFlags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel |
306
				                 ImGuiColorEditFlags_AlphaBar;
307
				im::Group([&]{
308
					ImGui::AlignTextToFramePadding();
309
					ImGui::TextUnformatted("Source (done)"sv);
310
					ImGui::AlignTextToFramePadding();
311
					ImGui::TextUnformatted("Source (todo)"sv);
312
				});
313
				ImGui::SameLine();
314
				im::Group([&]{
315
					ImGui::ColorEdit4("src-done", colorSrcDone.data(), colorFlags);
316
					ImGui::ColorEdit4("src-todo", colorSrcTodo.data(), colorFlags);
317
				});
318
				ImGui::SameLine(0.0f, ImGui::GetFontSize() * 3.0f);
319
				im::Group([&]{
320
					ImGui::AlignTextToFramePadding();
321
					ImGui::TextUnformatted("Destination (done)"sv);
322
					ImGui::AlignTextToFramePadding();
323
					ImGui::TextUnformatted("Destination (todo)"sv);
324
				});
325
				ImGui::SameLine();
326
				im::Group([&]{
327
					ImGui::ColorEdit4("dst-done", colorDstDone.data(), colorFlags);
328
					ImGui::ColorEdit4("dst-todo", colorDstTodo.data(), colorFlags);
329
				});
330
			});
331
332
			ImGui::TextUnformatted("Last command "sv);
333
			ImGui::SameLine();
334
			ImGui::TextUnformatted(inProgress ? "[in progress]"sv : "[finished]"sv);
335
336
			// Textual-representation of last command (grayed-out if finished).
337
			// Note:
338
			// * The representation _resembles_ to notation in MSX-BASIC, but is (intentionally) not
339
			//   the same. Here we need to convey more information (e.g. for copy commands we show
340
			//   both corners of the destination).
341
			// * For the non-block commands we don't clamp/wrap the coordinates.
342
			// * For the logical commands we don't mask the color register (e.g. to 4 bits for SCRN5).
343
			// * For the LINE command, we don't clamp/wrap the end-point.
344
			// * Src and dst rectangles are _separately_ clamped to left/right borders.
345
			//   That's different from the real VDP behavior, but maybe less confusing to show in the debugger.
346
			// * The first coordinate is always the start-corner. The second may be larger or
347
			//   smaller depending on 'dix' and 'diy'.
348
			// * The textual representation for commands that wrap around the top/bottom border is
349
			//   ambiguous (but the drawn overlay should be fine).
350
			im::DisabledIndent(!inProgress, [&]{
351
				ImGui::TextUnformatted(formatted.str);
352
			});
353
		});
354
355
		ImGui::Separator();
356
357
		auto palette = manager.palette->getPalette(vdp);
358
		if (color0 < 16) palette[0] = palette[color0];
359
360
		MemBuffer<uint32_t> pixels(512 * 256 * 4); // max size: screen 6/7, show all pages
361
		renderBitmap(vram.getData(), palette, mode, height, page,
362
				pixels.data());
363
		if (!bitmapTex) {
364
			bitmapTex.emplace(false, false); // no interpolation, no wrapping
365
		}
366
		bitmapTex->bind();
367
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
368
				GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
369
		int zx = (1 + bitmapZoom) * divX;
370
		int zy = (1 + bitmapZoom) * 2;
371
		auto zm = gl::vec2(float(zx), float(zy));
372
373
		gl::vec2 scrnPos;
374
		auto msxSize = gl::vec2(float(width), float(height));
375
		auto size = msxSize * zm;
376
		auto availSize = gl::vec2(ImGui::GetContentRegionAvail()) - gl::vec2(0.0f, ImGui::GetTextLineHeightWithSpacing());
377
		auto reqSize = size + gl::vec2(ImGui::GetStyle().ScrollbarSize);
378
		im::Child("##bitmap", min(availSize, reqSize), 0, ImGuiWindowFlags_HorizontalScrollbar, [&]{
379
			scrnPos = ImGui::GetCursorScreenPos();
380
			auto pos = ImGui::GetCursorPos();
381
			ImGui::Image(bitmapTex->getImGui(), size);
382
383
			if (bitmapGrid && (zx > 1) && (zy > 1)) {
384
				auto color = ImGui::ColorConvertFloat4ToU32(bitmapGridColor);
385
				for (auto y : xrange(zy)) {
386
					auto* line = &pixels[y * zx];
387
					for (auto x : xrange(zx)) {
388
						line[x] = (x == 0 || y == 0) ? color : 0;
389
					}
390
				}
391
				if (!bitmapGridTex) {
392
					bitmapGridTex.emplace(false, true); // no interpolation, with wrapping
393
				}
394
				bitmapGridTex->bind();
395
				glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, zx, zy, 0,
396
						GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
397
				ImGui::SetCursorPos(pos);
398
				ImGui::Image(bitmapGridTex->getImGui(), size, gl::vec2{}, msxSize);
399
			}
400
			auto* drawList = ImGui::GetWindowDrawList();
401
			if (rasterBeam) {
402
				auto center = scrnPos + (gl::vec2(rasterBeamPos) + gl::vec2{0.5f}) * zm;
403
				auto color = ImGui::ColorConvertFloat4ToU32(rasterBeamColor);
404
				auto thickness = zm.y * 0.5f;
405
				auto zm1 = 1.5f * zm;
406
				auto zm3 = 3.5f * zm;
407
				drawList->AddRect(center - zm, center + zm, color, 0.0f, thickness);
408
				drawList->AddLine(center - gl::vec2{zm1.x, 0.0f}, center - gl::vec2{zm3.x, 0.0f}, color, thickness);
409
				drawList->AddLine(center + gl::vec2{zm1.x, 0.0f}, center + gl::vec2{zm3.x, 0.0f}, color, thickness);
410
				drawList->AddLine(center - gl::vec2{0.0f, zm1.y}, center - gl::vec2{0.0f, zm3.y}, color, thickness);
411
				drawList->AddLine(center + gl::vec2{0.0f, zm1.y}, center + gl::vec2{0.0f, zm3.y}, color, thickness);
412
			}
413
414
			// draw the VDP command overlay
415
			auto offset = gl::vec2(0.0f, height > 256 ? 0.0f : float(256 * page));
416
			auto drawRect = [&](const Rect& r, ImU32 color) {
417
				auto tl = zm * (gl::vec2(r.p1) - offset);
418
				auto br = zm * (gl::vec2(r.p2) - offset + gl::vec2(1.0f)); // rect is inclusive, openGL is exclusive
419
				drawList->AddRectFilled(scrnPos + tl, scrnPos + br, color);
420
			};
421
			auto drawSplit = [&](const DoneTodo& dt, ImU32 colDone, ImU32 colTodo) {
422
				for (const auto& r : dt.done) drawRect(r, colDone);
423
				for (const auto& r : dt.todo) drawRect(r, colTodo);
424
			};
425
			auto drawOverlay = [&](const static_vector<Rect, 2>& rects, int x, int y, ImU32 colDone, ImU32 colTodo) {
426
				auto split1 = splitRect(rects[0], x, y);
427
				drawSplit(split1, colDone, colTodo);
428
				if (rects.size() == 2) {
429
					auto split2 = splitRect(rects[1], x, y);
430
					auto col1 = (split1.done.size() == 2) ? colTodo : colDone; // if (x,y) is in rects[0], then use todo-color
431
					drawSplit(split2, col1, colTodo);
432
				}
433
			};
434
			if ((showCmdOverlay == 2) || ((showCmdOverlay == 1) && inProgress)) {
435
				auto [sx2, sy2, dx2, dy2] = cmdEngine.getInprogressPosition();
436
				if (formatted.srcRect) {
437
					drawOverlay(*formatted.srcRect, sx2, sy2,
438
					            ImGui::ColorConvertFloat4ToU32(colorSrcDone),
439
					            ImGui::ColorConvertFloat4ToU32(colorSrcTodo));
440
				}
441
				if (formatted.dstRect) {
442
					drawOverlay(*formatted.dstRect, dx2, dy2,
443
					            ImGui::ColorConvertFloat4ToU32(colorDstDone),
444
					            ImGui::ColorConvertFloat4ToU32(colorDstTodo));
445
				}
446
			}
447
		});
448
		if (ImGui::IsItemHovered() && (mode != OTHER)) {
449
			auto [x, y] = trunc((gl::vec2(ImGui::GetIO().MousePos) - scrnPos) / zm);
450
			if ((0 <= x) && (x < width) && (0 <= y) && (y < height)) {
451
				auto dec3 = [&](int d) {
452
					im::ScopedFont sf(manager.fontMono);
453
					ImGui::SameLine(0.0f, 0.0f);
454
					ImGui::Text("%3d", d);
455
				};
456
				auto hex2 = [&](unsigned h) {
457
					im::ScopedFont sf(manager.fontMono);
458
					ImGui::SameLine(0.0f, 0.0f);
459
					ImGui::StrCat(hex_string<2>(h));
460
				};
461
				auto hex5 = [&](unsigned h) {
462
					im::ScopedFont sf(manager.fontMono);
463
					ImGui::SameLine(0.0f, 0.0f);
464
					ImGui::StrCat(hex_string<5>(h));
465
				};
466
467
				ImGui::TextUnformatted("x="sv); dec3(x);
468
				ImGui::SameLine();
469
				ImGui::TextUnformatted("y="sv); dec3(y);
470
471
				unsigned physAddr = 0x8000 * page + 128 * y;
472
				switch (mode) {
473
				case SCR5: physAddr += x / 2; break;
474
				case SCR6: physAddr += x / 4; break;
475
				case SCR7: physAddr += x / 4 + 0x08000 * (x & 2); break;
476
				case SCR8: case SCR11: case SCR12:
477
						physAddr += x / 2 + 0x10000 * (x & 1); break;
478
				default: assert(false);
479
				}
480
481
				auto value = vram.getData()[physAddr];
482
				auto color = [&] -> uint8_t {
483
					switch (mode) {
484
					case SCR5: case SCR7:
485
						return (value >> (4 * (1 - (x & 1)))) & 0x0f;
486
					case SCR6:
487
						return (value >> (2 * (3 - (x & 3)))) & 0x03;
488
					default:
489
						return value;
490
					}
491
				}();
492
				if (mode != one_of(SCR11, SCR12)) {
493
					ImGui::SameLine();
494
					ImGui::TextUnformatted("  color="sv); dec3(color);
495
				}
496
497
				ImGui::SameLine();
498
				ImGui::TextUnformatted("  vram: addr=0x");
499
				if (mode == one_of(SCR5, SCR6)) {
500
					hex5(physAddr);
501
				} else {
502
					unsigned logAddr = (physAddr & 0x0ffff) << 1 | (physAddr >> 16);
503
					hex5(logAddr),
504
					ImGui::SameLine(0.0f, 0.0f);
505
					ImGui::TextUnformatted("(log)/0x"sv); hex5(physAddr);
506
					ImGui::SameLine(0.0f, 0.0f);
507
					ImGui::TextUnformatted("(phys)"sv);
508
				}
509
				ImGui::SameLine();
510
				ImGui::TextUnformatted(" value=0x"sv); hex2(value);
511
			}
512
		}
513
	});
514
}
515
516
// TODO avoid code duplication with src/video/BitmapConverter
517
void ImGuiBitmapViewer::renderBitmap(std::span<const uint8_t> vram, std::span<const uint32_t, 16> palette16,
518
                                     int mode, int lines, int page, uint32_t* output) const
519
{
520
	auto yjk2rgb = [](int y, int j, int k) -> std::tuple<int, int, int> {
521
		// Note the formula for 'blue' differs from the 'traditional' formula
522
		// (e.g. as specified in the V9958 datasheet) in the rounding behavior.
523
		// Confirmed on real turbor machine. For details see:
524
		//    https://github.com/openMSX/openMSX/issues/1394
525
		//    https://twitter.com/mdpc___/status/1480432007180341251?s=20
526
		int r = std::clamp(y + j,                       0, 31);
527
		int g = std::clamp(y + k,                       0, 31);
528
		int b = std::clamp((5 * y - 2 * j - k + 2) / 4, 0, 31);
529
		return {r, g, b};
530
	};
531
532
	// TODO handle less than 128kB VRAM (will crash now)
533
	size_t addr = 0x8000 * page;
534
	switch (mode) {
535
	case SCR5:
536
		for (auto y : xrange(lines)) {
537
			auto* line = &output[256 * y];
538
			for (auto x : xrange(128)) {
539
				auto value = vram[addr];
540
				line[2 * x + 0] = palette16[(value >> 4) & 0x0f];
541
				line[2 * x + 1] = palette16[(value >> 0) & 0x0f];
542
				++addr;
543
			}
544
		}
545
		break;
546
547
	case SCR6:
548
		for (auto y : xrange(lines)) {
549
			auto* line = &output[512 * y];
550
			for (auto x : xrange(128)) {
551
				auto value = vram[addr];
552
				line[4 * x + 0] = palette16[(value >> 6) & 3];
553
				line[4 * x + 1] = palette16[(value >> 4) & 3];
554
				line[4 * x + 2] = palette16[(value >> 2) & 3];
555
				line[4 * x + 3] = palette16[(value >> 0) & 3];
556
				++addr;
557
			}
558
		}
559
		break;
560
561
	case SCR7:
562
		for (auto y : xrange(lines)) {
563
			auto* line = &output[512 * y];
564
			for (auto x : xrange(128)) {
565
				auto value0 = vram[addr + 0x00000];
566
				auto value1 = vram[addr + 0x10000];
567
				line[4 * x + 0] = palette16[(value0 >> 4) & 0x0f];
568
				line[4 * x + 1] = palette16[(value0 >> 0) & 0x0f];
569
				line[4 * x + 2] = palette16[(value1 >> 4) & 0x0f];
570
				line[4 * x + 3] = palette16[(value1 >> 0) & 0x0f];
571
				++addr;
572
			}
573
		}
574
		break;
575
576
	case SCR8: {
577
		auto toColor = [](uint8_t value) {
578
			int r = (value & 0x1c) >> 2;
579
			int g = (value & 0xe0) >> 5;
580
			int b = (value & 0x03) >> 0;
581
			int rr = (r << 5) | (r << 2) | (r >> 1);
582
			int gg = (g << 5) | (g << 2) | (g >> 1);
583
			int bb = (b << 6) | (b << 4) | (b << 2) | (b << 0);
584
			int aa = 255;
585
			return (rr << 0) | (gg << 8) | (bb << 16) | (aa << 24);
586
		};
587
		for (auto y : xrange(lines)) {
588
			auto* line = &output[256 * y];
589
			for (auto x : xrange(128)) {
590
				line[2 * x + 0] = toColor(vram[addr + 0x00000]);
591
				line[2 * x + 1] = toColor(vram[addr + 0x10000]);
592
				++addr;
593
			}
594
		}
595
		break;
596
	}
597
598
	case SCR11:
599
		for (auto y : xrange(lines)) {
600
			auto* line = &output[256 * y];
601
			for (auto x : xrange(64)) {
602
				std::array<unsigned, 4> p = {
603
					vram[addr + 0 + 0x00000],
604
					vram[addr + 0 + 0x10000],
605
					vram[addr + 1 + 0x00000],
606
					vram[addr + 1 + 0x10000],
607
				};
608
				addr += 2;
609
				int j = narrow<int>((p[2] & 7) + ((p[3] & 3) << 3)) - narrow<int>((p[3] & 4) << 3);
610
				int k = narrow<int>((p[0] & 7) + ((p[1] & 3) << 3)) - narrow<int>((p[1] & 4) << 3);
611
				for (auto n : xrange(4)) {
612
					uint32_t pix;
613
					if (p[n] & 0x08) {
614
						pix = palette16[p[n] >> 4];
615
					} else {
616
						int Y = narrow<int>(p[n] >> 3);
617
						auto [r, g, b] = yjk2rgb(Y, j, k);
618
						int rr = (r << 3) | (r >> 2);
619
						int gg = (g << 3) | (g >> 2);
620
						int bb = (b << 3) | (b >> 2);
621
						int aa = 255;
622
						pix = (rr << 0) | (gg << 8) | (bb << 16) | (aa << 24);
623
					}
624
					line[4 * x + n] = pix;
625
				}
626
			}
627
		}
628
		break;
629
630
	case SCR12:
631
		for (auto y : xrange(lines)) {
632
			auto* line = &output[256 * y];
633
			for (auto x : xrange(64)) {
634
				std::array<unsigned, 4> p = {
635
					vram[addr + 0 + 0x00000],
636
					vram[addr + 0 + 0x10000],
637
					vram[addr + 1 + 0x00000],
638
					vram[addr + 1 + 0x10000],
639
				};
640
				addr += 2;
641
				int j = narrow<int>((p[2] & 7) + ((p[3] & 3) << 3)) - narrow<int>((p[3] & 4) << 3);
642
				int k = narrow<int>((p[0] & 7) + ((p[1] & 3) << 3)) - narrow<int>((p[1] & 4) << 3);
643
				for (auto n : xrange(4)) {
644
					int Y = narrow<int>(p[n] >> 3);
645
					auto [r, g, b] = yjk2rgb(Y, j, k);
646
					int rr = (r << 3) | (r >> 2);
647
					int gg = (g << 3) | (g >> 2);
648
					int bb = (b << 3) | (b >> 2);
649
					int aa = 255;
650
					line[4 * x + n] = (rr << 0) | (gg << 8) | (bb << 16) | (aa << 24);
651
				}
652
			}
653
		}
654
		break;
655
656
	case OTHER:
657
		for (auto y : xrange(lines)) {
658
			auto* line = &output[256 * y];
659
			for (auto x : xrange(256)) {
660
				line[x] = getColor(imColor::GRAY);
661
			}
662
		}
663
		break;
664
	}
665
}
666
667
} // namespace openmsx