OXYGENHUB
◆ main openmsx / src / imgui / ImGuiSpriteViewer.cc
28.4 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "ImGuiSpriteViewer.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiManager.hh"
5
#include "ImGuiPalette.hh"
6
#include "ImGuiUtils.hh"
7
8
#include "MSXMotherBoard.hh"
9
#include "SpriteChecker.hh"
10
#include "SpriteConverter.hh"
11
#include "VDP.hh"
12
#include "VDPVRAM.hh"
13
14
#include "xrange.hh"
15
16
#include <imgui.h>
17
18
#include <cassert>
19
#include <cstdint>
20
#include <ranges>
21
#include <span>
22
23
namespace openmsx {
24
25
using namespace std::literals;
26
27
ImGuiSpriteViewer::ImGuiSpriteViewer(ImGuiManager& manager_, size_t index)
28
	: ImGuiPart(manager_)
29
	, title(strCat("Sprite viewer", strCat_if(index, " (", index + 1, ')')))
30
{
31
}
32
33
void ImGuiSpriteViewer::save(ImGuiTextBuffer& buf)
34
{
35
	savePersistent(buf, *this, persistentElements);
36
}
37
38
void ImGuiSpriteViewer::loadLine(std::string_view name, zstring_view value)
39
{
40
	loadOnePersistent(name, value, *this, persistentElements);
41
}
42
43
static void draw8(uint8_t pattern, uint32_t fgCol, uint32_t bgCol, std::span<uint32_t, 8> out)
44
{
45
	out[0] = (pattern & 0x80) ? fgCol : bgCol;
46
	out[1] = (pattern & 0x40) ? fgCol : bgCol;
47
	out[2] = (pattern & 0x20) ? fgCol : bgCol;
48
	out[3] = (pattern & 0x10) ? fgCol : bgCol;
49
	out[4] = (pattern & 0x08) ? fgCol : bgCol;
50
	out[5] = (pattern & 0x04) ? fgCol : bgCol;
51
	out[6] = (pattern & 0x02) ? fgCol : bgCol;
52
	out[7] = (pattern & 0x01) ? fgCol : bgCol;
53
}
54
55
static void renderPatterns8(const VramTable& pat, std::span<uint32_t> output)
56
{
57
	auto zero = getColor(imColor::TRANSPARENT);
58
	auto one  = getColor(imColor::TEXT);
59
	for (auto row : xrange(8)) {
60
		for (auto column : xrange(32)) {
61
			auto patNum = 32 * row + column;
62
			auto offset = 8 * patNum;
63
			for (auto y : xrange(8)) {
64
				auto pattern = pat[offset + y];
65
				auto out = subspan<8>(output, (8 * row + y) * 256 + 8 * column);
66
				draw8(pattern, one, zero, out);
67
			}
68
		}
69
	}
70
}
71
72
static void renderPatterns16(const VramTable& pat, std::span<uint32_t> output)
73
{
74
	auto zero = getColor(imColor::TRANSPARENT);
75
	auto one  = getColor(imColor::TEXT);
76
	for (auto row : xrange(4)) {
77
		for (auto column : xrange(16)) {
78
			auto patNum = 4 * (16 * row + column);
79
			auto offset = 8 * patNum;
80
			for (auto y : xrange(16)) {
81
				auto patternA = pat[offset + y +  0];
82
				auto patternB = pat[offset + y + 16];
83
				auto outA = subspan<8>(output, (16 * row + y) * 256 + 16 * column +  0);
84
				auto outB = subspan<8>(output, (16 * row + y) * 256 + 16 * column +  8);
85
				draw8(patternA, one, zero, outA);
86
				draw8(patternB, one, zero, outB);
87
			}
88
		}
89
	}
90
}
91
92
[[nodiscard]] static int getSpriteAttrAddr(int sprite, int mode)
93
{
94
	return (mode == 2 ? 512 : 0) + 4 * sprite;
95
}
96
[[nodiscard]] static int getSpriteColorAddr(int sprite, int mode)
97
{
98
	assert(mode == 2); (void)mode;
99
	return 16 * sprite;
100
}
101
102
static void renderSpriteAttrib(const VramTable& att, int sprite, int mode, int size, int transparent,
103
                               float zoom, std::span<uint32_t, 16> palette, ImTextureID patternTex)
104
{
105
	int addr = getSpriteAttrAddr(sprite, mode);
106
	int pattern = att[addr + 2];
107
	if (size == 16) pattern /= 4;
108
109
	int patternsPerRow = 256 / size;
110
	int cc = pattern % patternsPerRow;
111
	int rr = pattern / patternsPerRow;
112
	float u1 = float(cc + 0) / float(patternsPerRow);
113
	float u2 = float(cc + 1) / float(patternsPerRow);
114
	float v1 = float(size * rr) * (1.0f / 64.0f);
115
116
	auto getColor = [&](int color) -> gl::vec4 {
117
		if (color == 0 && transparent) return {};
118
		return ImGui::ColorConvertU32ToFloat4(palette[color]);
119
	};
120
121
	if (mode == 1) {
122
		auto attrib = att[addr + 3];
123
		auto color = attrib & 0x0f;
124
		float v2 = float(size * (rr + 1)) * (1.0f / 64.0f);
125
		ImGui::ImageWithBg(patternTex, zoom * gl::vec2{float(size)}, {u1, v1}, {u2, v2},
126
		                   {}, getColor(color));
127
	} else {
128
		int colorBase = getSpriteColorAddr(sprite, mode);
129
		gl::vec2 pos = ImGui::GetCursorPos();
130
		for (auto y : xrange(size)) {
131
			auto attrib = att[colorBase + y];
132
			auto color = attrib & 0x0f;
133
			ImGui::SetCursorPos({pos.x, pos.y + zoom * float(y)});
134
			float v2 = v1 + (1.0f / 64.0f);
135
			ImGui::ImageWithBg(patternTex, zoom * gl::vec2{float(size), 1.0f}, {u1, v1}, {u2, v2},
136
			                   {}, getColor(color));
137
			v1 = v2;
138
		}
139
	}
140
}
141
142
void ImGuiSpriteViewer::paint(MSXMotherBoard* motherBoard)
143
{
144
	if (!show || !motherBoard) return;
145
146
	ImGui::SetNextWindowSize({748, 1010}, ImGuiCond_FirstUseEver);
147
	im::Window(title.c_str(), &show, [&]{
148
		auto* vdp = dynamic_cast<VDP*>(motherBoard->findDevice("VDP")); // TODO name based OK?
149
		if (!vdp) return;
150
		const auto& vram = vdp->getVRAM().getData();
151
152
		auto modeToStr = [](int mode) {
153
			if (mode == 0) return "no sprites";
154
			if (mode == 1) return "1";
155
			if (mode == 2) return "2";
156
			assert(false); return "ERROR";
157
		};
158
		auto sizeToStr = [](int size) {
159
			if (size ==  8) return "8 x 8";
160
			if (size == 16) return "16 x 16";
161
			assert(false); return "ERROR";
162
		};
163
		auto yesNo = [](int x) {
164
			if (x == 0) return "no";
165
			if (x == 1) return "yes";
166
			assert(false); return "ERROR";
167
		};
168
		auto attMult = [](int mode) { return 1 << ((mode == 2) ? 10 : 7); };
169
170
		bool isMSX1 = vdp->isMSX1VDP();
171
		auto displayMode = vdp->getDisplayMode();
172
		bool planar = displayMode.isPlanar();
173
		int vdpMode = displayMode.getSpriteMode(isMSX1);
174
		int vdpVerticalScroll = vdp->getVerticalScroll();
175
		int vdpLines = vdp->getNumberOfLines();
176
177
		int vdpSize = vdp->getSpriteSize();
178
		int vdpMag = vdp->isSpriteMag();
179
		int vdpTransparent = vdp->getTransparency();
180
181
		int vdpPatBase = vdp->getSpritePatternTableBase();
182
		int vdpAttBase = vdp->getSpriteAttributeTableBase() & ~(attMult(vdpMode) - 1);
183
184
		auto vramSize = std::min(vdp->getVRAM().getSize(), 0x20000u); // max 128kB
185
186
		auto palette = manager.palette->getPalette(vdp);
187
		// TODO? if (color0 < 16) palette[0] = palette[color0];
188
189
		bool manMode   = overrideAll || overrideMode;
190
		bool manSize   = overrideAll || overrideSize;
191
		bool manMag    = overrideAll || overrideMag;
192
		bool manTrans  = overrideAll || overrideTrans;
193
		bool manPat    = overrideAll || overridePat;
194
		bool manAtt    = overrideAll || overrideAtt;
195
		bool manScroll = overrideAll || overrideScroll;
196
		bool manLines  = overrideAll || overrideLines;
197
198
		im::TreeNode("Settings", ImGuiTreeNodeFlags_DefaultOpen, [&]{
199
			im::Group([&]{
200
				ImGui::TextUnformatted("VDP settings");
201
				im::Disabled(manMode, [&]{
202
					ImGui::AlignTextToFramePadding();
203
					ImGui::StrCat("Sprite mode: ", modeToStr(vdpMode));
204
				});
205
				im::Disabled(manSize, [&]{
206
					ImGui::AlignTextToFramePadding();
207
					ImGui::StrCat("Sprite size: ", sizeToStr(vdpSize));
208
				});
209
				im::Disabled(manMag, [&]{
210
					ImGui::AlignTextToFramePadding();
211
					ImGui::StrCat("Sprites magnified: ", yesNo(vdpMag));
212
				});
213
				im::Disabled(manTrans, [&]{
214
					ImGui::AlignTextToFramePadding();
215
					ImGui::StrCat("Color 0 transparent: ", yesNo(vdpTransparent));
216
				});
217
				im::Disabled(manPat, [&]{
218
					ImGui::AlignTextToFramePadding();
219
					ImGui::StrCat("Pattern table: 0x", hex_string<5>(vdpPatBase));
220
				});
221
				im::Disabled(manAtt, [&]{
222
					ImGui::AlignTextToFramePadding();
223
					ImGui::StrCat("Attribute table: 0x", hex_string<5>(vdpAttBase | (vdpMode == 2 ? 512 : 0)));
224
				});
225
				im::Disabled(manScroll, [&]{
226
					ImGui::AlignTextToFramePadding();
227
					ImGui::StrCat("Vertical scroll: ", vdpVerticalScroll);
228
				});
229
				im::Disabled(manLines, [&]{
230
					ImGui::AlignTextToFramePadding();
231
					ImGui::StrCat("Visible lines: ", (vdpLines == 192) ? "192"sv : "212"sv);
232
				});
233
			});
234
			ImGui::SameLine();
235
			im::Group([&]{
236
				ImGui::Checkbox("Manual override", &overrideAll);
237
				im::Group([&]{
238
					im::Disabled(overrideAll, [&]{
239
						ImGui::Checkbox("##o-mode",   overrideAll ? &overrideAll : &overrideMode);
240
						ImGui::Checkbox("##o-size",   overrideAll ? &overrideAll : &overrideSize);
241
						ImGui::Checkbox("##o-mag",    overrideAll ? &overrideAll : &overrideMag);
242
						ImGui::Checkbox("##o-trans",  overrideAll ? &overrideAll : &overrideTrans);
243
						ImGui::Checkbox("##o-pat",    overrideAll ? &overrideAll : &overridePat);
244
						ImGui::Checkbox("##o-att",    overrideAll ? &overrideAll : &overrideAtt);
245
						ImGui::Checkbox("##o-scroll", overrideAll ? &overrideAll : &overrideScroll);
246
						ImGui::Checkbox("##o-lines",  overrideAll ? &overrideAll : &overrideLines);
247
					});
248
				});
249
				ImGui::SameLine();
250
				im::Group([&]{
251
					im::ItemWidth(ImGui::GetFontSize() * 9.0f, [&]{
252
						im::Disabled(!manMode, [&]{
253
							if (manMode && isMSX1) manualMode = 1;
254
							im::Combo("##mode", modeToStr(manualMode), [&]{
255
								if (ImGui::Selectable("1")) manualMode = 1;
256
								if (!isMSX1 && ImGui::Selectable("2")) manualMode = 2;
257
							});
258
						});
259
						im::Disabled(!manSize, [&]{
260
							im::Combo("##size", sizeToStr(manualSize), [&]{
261
								if (ImGui::Selectable(" 8 x  8")) manualSize =  8;
262
								if (ImGui::Selectable("16 x 16")) manualSize = 16;
263
							});
264
						});
265
						im::Disabled(!manMag, [&]{
266
							im::Combo("##mag", yesNo(manualMag), [&]{
267
								if (ImGui::Selectable("no"))  manualMag = 0;
268
								if (ImGui::Selectable("yes")) manualMag = 1;
269
							});
270
						});
271
						im::Disabled(!manTrans, [&]{
272
							im::Combo("##trans", yesNo(manualTransparent), [&]{
273
								if (ImGui::Selectable("no"))  manualTransparent = 0;
274
								if (ImGui::Selectable("yes")) manualTransparent = 1;
275
							});
276
						});
277
						im::Disabled(!manPat, [&]{
278
							comboHexSequence<5>("##pat", &manualPatBase, 8 * 256, vramSize, 0);
279
						});
280
						im::Disabled(!manAtt, [&]{
281
							comboHexSequence<5>("##att", &manualAttBase, attMult(manualMode), vramSize, manualMode == 2 ? 512 : 0);
282
						});
283
						im::Disabled(!manScroll, [&]{
284
							ImGui::InputInt("##verticalScroll", &manualVerticalScroll);
285
							manualVerticalScroll &= 0xff;
286
						});
287
						im::Disabled(!manLines, [&]{
288
							ImGui::Combo("##lines", &manualLines, "192\000212\000256\000");
289
						});
290
					});
291
				});
292
			});
293
			ImGui::SameLine();
294
			ImGui::Dummy(ImVec2(25, 1));
295
			ImGui::SameLine();
296
			im::Group([&]{
297
				ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10.0f);
298
				ImGui::Combo("Palette", &manager.palette->whichPalette, "VDP\000Custom\000Fixed\000");
299
				if (ImGui::Button("Open palette editor")) {
300
					manager.palette->window.raise();
301
				}
302
				ImGui::Separator();
303
				ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.0f);
304
				ImGui::Combo("Zoom", &zoom, "1x\0002x\0003x\0004x\0005x\0006x\0007x\0008x\000");
305
				ImGui::Checkbox("grid", &grid);
306
				ImGui::SameLine();
307
				im::Disabled(!grid, [&]{
308
					ImGui::ColorEdit4("Grid color", gridColor.data(),
309
						ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaBar);
310
				});
311
312
				ImGui::TextUnformatted("Checkerboard:"sv);
313
				simpleToolTip("Used as background in 'Sprite attribute' and 'Rendered sprites' view");
314
				ImGui::SameLine();
315
				auto checkerBoardColorFlags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoAlpha;
316
				ImGui::ColorEdit4("checkerboard color1", checkerBoardColor1.data(), checkerBoardColorFlags);
317
				ImGui::SameLine();
318
				ImGui::ColorEdit4("checkerboard color2", checkerBoardColor2.data(), checkerBoardColorFlags);
319
				im::Indent([&]{
320
					ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6.0f);
321
					ImGui::InputInt("size", &checkerBoardSize);
322
				});
323
			});
324
		});
325
		ImGui::Separator();
326
327
		auto copySpriteDataPopup = [&](zstring_view text, function_ref<std::string()> supplier) {
328
			return im::PopupContextWindow("CopySpriteDataPopup", [&]{
329
				if (ImGui::MenuItem(text.c_str())) {
330
					ImGui::SetClipboardText(supplier().c_str());
331
				}
332
			});
333
		};
334
		auto formatClipboardData = [&](unsigned address, unsigned size) {
335
			return formatToString([&](unsigned addr) { return vram[addr]; }, address, address + size - 1, {}, 1, {}, ", ", "%02X", manager.getInterpreter());
336
		};
337
338
		int mode = manMode ? manualMode : vdpMode;
339
		int size = manSize ? manualSize : vdpSize;
340
		int mag  = manMag ? manualMag  : vdpMag;
341
		int verticalScroll = manScroll ? manualVerticalScroll : vdpVerticalScroll;
342
		int lines = manLines ? (manualLines == 0 ? 192 :
343
		                        manualLines == 1 ? 212 :
344
		                                           256)
345
		                     : vdpLines;
346
		int transparent  = manTrans ? manualTransparent  : vdpTransparent;
347
348
		VramTable patTable(vram, planar);
349
		unsigned patReg = (manPat ? (manualPatBase | ((8 * 256) - 1)) : vdp->getSpritePatternTableBase()) >> 11;
350
		patTable.setRegister(patReg, 11);
351
		patTable.setIndexSize(11);
352
353
		VramTable attTable(vram, planar);
354
		unsigned attReg = (manAtt ? (manualAttBase | (attMult(manualMode) - 1)) : vdp->getSpriteAttributeTableBase()) >> 7;
355
		attTable.setRegister(attReg, 7);
356
		attTable.setIndexSize((mode == 2) ? 10 : 7);
357
358
		// create pattern texture
359
		if (!patternTex.get()) {
360
			patternTex = gl::Texture(false, false); // no interpolation, no wrapping
361
		}
362
		patternTex.bind();
363
		std::array<uint32_t, 256 * 64> pixels;
364
		if (mode != 0) {
365
			if (size == 8) {
366
				renderPatterns8 (patTable, pixels);
367
			} else {
368
				renderPatterns16(patTable, pixels);
369
			}
370
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 64, 0,
371
			             GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
372
		} else {
373
			pixels[0] = getColor(imColor::GRAY);
374
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0,
375
			             GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
376
		}
377
378
		// create grid texture
379
		int zm = 2 * (1 + zoom);
380
		auto gColor = ImGui::ColorConvertFloat4ToU32(gridColor);
381
		if (grid) {
382
			size_t gridSize = size * zm;
383
			for (auto y : xrange(gridSize)) {
384
				auto* line = &pixels[y * gridSize];
385
				for (auto x : xrange(gridSize)) {
386
					line[x] = (x == 0 || y == 0) ? gColor : 0;
387
				}
388
			}
389
			if (!gridTex.get()) {
390
				gridTex = gl::Texture(false, true); // no interpolation, with wrapping
391
			}
392
			gridTex.bind();
393
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
394
			             narrow<int>(gridSize), narrow<int>(gridSize),
395
			             0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
396
		}
397
398
		// create checker board texture
399
		if (checkerBoardSize) {
400
			pixels[0] = pixels[3] = ImGui::ColorConvertFloat4ToU32(checkerBoardColor1);
401
			pixels[1] = pixels[2] = ImGui::ColorConvertFloat4ToU32(checkerBoardColor2);
402
			if (!checkerTex.get()) {
403
				checkerTex = gl::Texture(false, true); // no interpolation, with wrapping
404
			}
405
			checkerTex.bind();
406
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 2, 0,
407
			             GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
408
		}
409
		im::TreeNode("Sprite patterns", ImGuiTreeNodeFlags_DefaultOpen, [&]{
410
			auto fullSize = gl::vec2(256, 64) * float(zm);
411
			im::Child("##pattern", {0, fullSize.y}, 0, ImGuiWindowFlags_HorizontalScrollbar, [&]{
412
				auto pos1 = ImGui::GetCursorPos();
413
				gl::vec2 scrnPos = ImGui::GetCursorScreenPos();
414
				ImGui::Image(patternTex.getImGui(), fullSize);
415
				gl::vec2 zoomPatSize{float(size * zm)};
416
				bool hovered = ImGui::IsItemHovered() && (mode != 0);
417
				if (hovered) {
418
					gridPosition = trunc((gl::vec2(ImGui::GetIO().MousePos) - scrnPos) / zoomPatSize);
419
				}
420
				ImGui::SameLine();
421
				im::Group([&]{
422
					auto pattern = (size == 16) ? ((16 * gridPosition.y) + gridPosition.x) * 4
423
												: ((32 * gridPosition.y) + gridPosition.x) * 1;
424
					bool popup = copySpriteDataPopup("Copy pattern data to clipboard", [&] {
425
						return formatClipboardData(patTable.getAddress(8 * pattern), size == 16 ? 32 : 8);
426
					});
427
					if (hovered || popup) {
428
						ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 1); // HACK !!
429
						ImGui::StrCat("pattern: ", pattern);
430
						ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 1); // HACK !!
431
						ImGui::StrCat("address: 0x", hex_string<5>(patTable.getAddress(8 * pattern)));
432
						ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 1); // HACK !!
433
						auto recipPatTex = recip((size == 16) ? gl::vec2{16, 4} : gl::vec2{32, 8});
434
						auto uv1 = gl::vec2(gridPosition) * recipPatTex;
435
						auto uv2 = uv1 + recipPatTex;
436
						auto pos2 = ImGui::GetCursorPos();
437
						int z = (size == 16) ? 3 : 6;
438
						ImGui::Image(patternTex.getImGui(), float(z) * zoomPatSize, uv1, uv2);
439
						if (grid) {
440
							if (!zoomGridTex.get()) {
441
								zoomGridTex = gl::Texture(false, true); // no interpolation, with wrapping
442
							}
443
							int s = z * zm;
444
							for (auto y : xrange(s)) {
445
								auto* line = &pixels[y * s];
446
								for (auto x : xrange(s)) {
447
									line[x] = (x == 0 || y == 0) ? gColor : 0;
448
								}
449
							}
450
							zoomGridTex.bind();
451
							glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, s, s, 0,
452
								GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
453
							ImGui::SetCursorPos(pos2);
454
							ImGui::Image(zoomGridTex.getImGui(),
455
							             float(z) * zoomPatSize, {}, gl::vec2{float(size)});
456
						}
457
					} else {
458
						ImGui::Dummy(zoomPatSize);
459
					}
460
				});
461
				if (grid) {
462
					ImGui::SetCursorPos(pos1);
463
					ImGui::Image(gridTex.getImGui(), fullSize,
464
						{}, (size == 8) ? gl::vec2{32.0f, 8.0f} : gl::vec2{16.0f, 4.0f});
465
				}
466
			});
467
		});
468
		ImGui::Separator();
469
470
		im::TreeNode("Sprite attributes", ImGuiTreeNodeFlags_DefaultOpen, [&]{
471
			auto zoomSize = float(zm * size);
472
			auto fullSize = zoomSize * gl::vec2(8, 4);
473
			auto hoverY = 3.0f * (ImGui::GetTextLineHeight() - 1.0f)
474
			            + float(3 * zm) * float(size);
475
			im::Child("##attrib", {0, std::max(fullSize.y, hoverY)}, 0, ImGuiWindowFlags_HorizontalScrollbar, [&]{
476
				if (mode == 0) {
477
					ImGui::TextUnformatted("No sprites in this screen mode"sv);
478
				} else {
479
					gl::vec2 topLeft = ImGui::GetCursorPos();
480
					gl::vec2 scrnPos = ImGui::GetCursorScreenPos();
481
					if (checkerBoardSize) {
482
						ImGui::SetCursorPos(topLeft);
483
						ImGui::Image(checkerTex.getImGui(), fullSize,
484
							{}, fullSize / (4.0f * float(checkerBoardSize)));
485
					}
486
					for (auto row : xrange(4)) {
487
						for (auto column : xrange(8)) {
488
							int sprite = 8 * row + column;
489
							ImGui::SetCursorPos(topLeft + zoomSize * gl::vec2(float(column), float(row)));
490
							renderSpriteAttrib(attTable, sprite, mode, size, transparent,
491
							                   float(zm), palette, patternTex.getImGui());
492
						}
493
					}
494
					ImGui::SetCursorPos(topLeft);
495
					if (grid) {
496
						ImGui::Image(gridTex.getImGui(), fullSize,
497
							{}, gl::vec2{8, 4});
498
					} else {
499
						ImGui::Dummy(fullSize);
500
					}
501
					gl::vec2 zoomPatSize{float(size * zm)};
502
					bool hovered = ImGui::IsItemHovered();
503
					if (hovered) {
504
						gridPosition = trunc((gl::vec2(ImGui::GetIO().MousePos) - scrnPos) / zoomPatSize);
505
					}
506
					auto sprite = 8 * gridPosition.y + gridPosition.x;
507
					auto addr = getSpriteAttrAddr(sprite, mode);
508
					bool popup = (mode == 2) && copySpriteDataPopup("Copy color data to clipboard", [&]{
509
						return formatClipboardData(attTable.getAddress(getSpriteColorAddr(sprite, mode)), size);
510
					});
511
					if (hovered || popup) {
512
						ImGui::SameLine();
513
						im::Group([&]{
514
							ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 1); // HACK !!
515
							ImGui::StrCat("sprite: ", sprite);
516
							ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 1); // HACK !!
517
							ImGui::StrCat("address: 0x", hex_string<5>(attTable.getAddress(addr)));
518
							ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 1); // HACK !!
519
							auto pos = ImGui::GetCursorPos();
520
							if (checkerBoardSize) {
521
								ImGui::Image(checkerTex.getImGui(), 3.0f * zoomPatSize,
522
									{}, zoomPatSize / (4.0f * float(checkerBoardSize)));
523
							}
524
							ImGui::SetCursorPos(pos);
525
							renderSpriteAttrib(attTable, sprite, mode, size, transparent,
526
							                   float(3 * zm), palette, patternTex.getImGui());
527
						});
528
						ImGui::SameLine();
529
						im::Group([&]{
530
							ImGui::StrCat("x: ", attTable[addr + 1],
531
							              "  y: ", attTable[addr + 0]);
532
							ImGui::StrCat("pattern: ", attTable[addr + 2]);
533
							if (mode == 1) {
534
								auto c = attTable[addr + 3];
535
								ImGui::StrCat("color: ", c & 15, (c & 80 ? " (EC)"sv : ""sv));
536
							} else {
537
								int colorBase = getSpriteColorAddr(sprite, mode);
538
								im::StyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1), [&]{ // Tighten spacing
539
									ImGui::TextUnformatted("Colors per line (hex):"sv);
540
									for (auto y : xrange(4)) {
541
										for (auto x : xrange(4)) {
542
											auto line = 4 * y + x;
543
											auto a = attTable[colorBase + line];
544
											ImGui::StrCat(hex_string<1>(line), ": ",
545
											              hex_string<1>(a & 15),
546
											              (a & 0xe0 ? '*' : ' '),
547
											              ' ');
548
											if (x != 3) ImGui::SameLine();
549
										}
550
									}
551
									ImGui::StrCat("address: 0x", hex_string<5>(attTable.getAddress(colorBase)));
552
								});
553
							}
554
						});
555
					}
556
				}
557
			});
558
		});
559
		ImGui::Separator();
560
561
		im::TreeNode("Rendered sprites", ImGuiTreeNodeFlags_DefaultOpen, [&]{
562
			struct SpriteBox {
563
				int x, y, w, h; // box
564
				uint8_t sprite;
565
				uint8_t vramX, vramY;
566
				uint8_t pattern;
567
			};
568
569
			std::array<uint8_t, 256> spriteCount = {}; // zero initialize
570
			std::array<std::array<SpriteChecker::SpriteInfo, 32 + 1>, 256> spriteBuffer; // uninitialized
571
			std::array<SpriteBox, 32> spriteBoxes; // uninitialized
572
573
			uint8_t spriteLimit = (mode == 1) ? 4 : 8;
574
			uint8_t stopY = (mode == 1) ? 208 : 216;
575
			uint8_t patMask = (size == 8) ? 0xff : 0xfc;
576
			int magFactor = mag ? 2 : 1;
577
			auto magSize = magFactor * size;
578
579
			uint8_t spriteCnt = 0;
580
			for (/**/; spriteCnt < 32; ++spriteCnt) {
581
				int addr = getSpriteAttrAddr(spriteCnt, mode);
582
				uint8_t originalY = attTable[addr + 0];
583
				if (enableStopY && (originalY == stopY)) break;
584
				auto y = uint8_t(originalY + 1 - verticalScroll);
585
				int initialY = y;
586
587
				uint8_t x    = attTable[addr + 1];
588
				uint8_t pat  = attTable[addr + 2] & patMask;
589
				uint8_t att1 = attTable[addr + 3]; // only mode 1
590
591
				bool anyEC = false;
592
				bool anyNonEC = false;
593
				for (int spriteY : xrange(size)) { // each line in the sprite
594
					auto attr = [&]{
595
						if (mode != 2) return att1;
596
						int colorBase = getSpriteColorAddr(spriteCnt, mode);
597
						return attTable[colorBase + spriteY];
598
					}();
599
600
					bool EC = attr & 0x80;
601
					(EC ? anyEC : anyNonEC) = true;
602
					int xx = EC ? x - 32 : x;
603
604
					auto pattern = [&]{
605
						uint8_t p0 = patTable[8 * pat + spriteY +  0];
606
						SpriteChecker::SpritePattern result = p0 << 24;
607
						if (size == 8) return result;
608
						uint8_t p1 = patTable[8 * pat + spriteY + 16];
609
						return result | (p1 << 16);
610
					}();
611
					if (mag) pattern = SpriteChecker::doublePattern(pattern);
612
613
					for ([[maybe_unused]] int mm : xrange(magFactor)) {
614
						auto count = spriteCount[y];
615
						if (!enableLimitPerLine || (count < spriteLimit)) {
616
							auto& spr = spriteBuffer[y][count];
617
							spr.pattern = pattern;
618
							spr.x = narrow<int16_t>(xx);
619
							spr.colorAttrib = attr;
620
621
							spriteCount[y] = count + 1;
622
							spriteBuffer[y][count + 1].colorAttrib = 0; // sentinel (mode 2)
623
						}
624
						++y; // wraps 256->0
625
					}
626
				}
627
				assert(anyEC || anyNonEC);
628
				spriteBoxes[spriteCnt] = SpriteBox{
629
					.x = anyEC ? x - 32 : x,
630
					.y = initialY,
631
					.w = magSize + (anyEC && anyNonEC ? 32 : 0),
632
					.h = magSize,
633
					.sprite = spriteCnt,
634
					.vramX = x,
635
					.vramY = originalY,
636
					.pattern = pat};
637
			}
638
639
			std::array<uint32_t, 256 * 256> screen; // TODO screen6 striped colors
640
			memset(screen.data(), 0, sizeof(uint32_t) * 256 * lines); // transparent
641
			for (auto line : xrange(lines)) {
642
				auto count = spriteCount[line];
643
				if (count == 0) continue;
644
				auto lineBuf = subspan<256>(screen, 256 * line);
645
646
				if (mode == 1) {
647
					auto visibleSprites = subspan(spriteBuffer[line], 0, count);
648
					for (const auto& spr : std::views::reverse(visibleSprites)) {
649
						uint8_t colIdx = spr.colorAttrib & 0x0f;
650
						if (colIdx == 0 && transparent) continue;
651
						auto color = palette[colIdx];
652
653
						auto pattern = spr.pattern;
654
						int x = spr.x;
655
						if (!SpriteConverter::clipPattern(x, pattern, 0, 256)) continue;
656
657
						while (pattern) {
658
							if (pattern & 0x8000'0000) {
659
								lineBuf[x] = color;
660
							}
661
							pattern <<= 1;
662
							++x;
663
						}
664
					}
665
				} else if (mode == 2) {
666
					auto visibleSprites = subspan(spriteBuffer[line], 0, count + 1); // +1 for sentinel
667
668
					// see SpriteConverter
669
					int first = 0;
670
					while (true /*sentinel*/) {
671
						if ((visibleSprites[first].colorAttrib & 0x40) == 0) [[likely]] {
672
							break;
673
						}
674
						++first;
675
					}
676
					for (int i = narrow<int>(count - 1); i >= first; --i) {
677
						const auto& spr = visibleSprites[i];
678
						uint8_t c = spr.colorAttrib & 0x0F;
679
						if (c == 0 && transparent) continue;
680
681
						auto pattern = spr.pattern;
682
						int x = spr.x;
683
						if (!SpriteConverter::clipPattern(x, pattern, 0, 256)) continue;
684
685
						while (pattern) {
686
							if (pattern & 0x8000'0000) {
687
								uint8_t color = c;
688
								// Merge in any following CC=1 sprites.
689
								for (int j = i + 1; /*sentinel*/; ++j) {
690
									const auto& info2 = visibleSprites[j];
691
									if (!(info2.colorAttrib & 0x40)) break;
692
									unsigned shift2 = x - info2.x;
693
									if ((shift2 < 32) &&
694
									((info2.pattern << shift2) & 0x8000'0000)) {
695
										color |= info2.colorAttrib & 0x0F;
696
									}
697
								}
698
								// TODO screen 6
699
								//	auto pixL = palette[color >> 2];
700
								//	auto pixR = palette[color & 3];
701
								//	lineBuf[x * 2 + 0] = pixL;
702
								//	lineBuf[x * 2 + 1] = pixR;
703
								lineBuf[x] = palette[color];
704
							}
705
							++x;
706
							pattern <<= 1;
707
						}
708
					}
709
				}
710
			}
711
			if (!renderTex.get()) {
712
				renderTex = gl::Texture(false, true); // no interpolation, with wrapping
713
			}
714
			renderTex.bind();
715
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, lines, 0,
716
			             GL_RGBA, GL_UNSIGNED_BYTE, screen.data());
717
718
			std::array<SpriteBox, 2 * 32> clippedBoxes;
719
			int nrClippedBoxes = 0;
720
			auto addClippedBox = [&](SpriteBox b) {
721
				assert(nrClippedBoxes < 64);
722
				clippedBoxes[nrClippedBoxes] = b;
723
				++nrClippedBoxes;
724
			};
725
			for (int sprite : xrange(spriteCnt)) {
726
				const auto& b = spriteBoxes[sprite];
727
				int x = b.x;
728
				int y = b.y;
729
				int w = b.w;
730
				int h = b.h;
731
				if (x < 0) {
732
					w += x;
733
					if (w <= 0) continue;
734
					x = 0;
735
				} else if (x + w > 256) {
736
					w = 256 - x;
737
				}
738
739
				int yEnd = y + h;
740
				if (yEnd < 256) {
741
					addClippedBox(SpriteBox{x, y, w, h, b.sprite, b.vramX, b.vramY, b.pattern});
742
				} else {
743
					addClippedBox(SpriteBox{x, y, w, 256 - y, b.sprite, b.vramX, b.vramY, b.pattern});
744
					addClippedBox(SpriteBox{x, 0, w, yEnd - 256, b.sprite, b.vramX, b.vramY, b.pattern});
745
				}
746
			}
747
748
			auto fullSize = float(zm) * gl::vec2(256, float(lines));
749
			im::Child("##screen", {0.0f, fullSize.y}, 0, ImGuiWindowFlags_HorizontalScrollbar, [&]{
750
				auto* drawList = ImGui::GetWindowDrawList();
751
				gl::vec2 scrnPos = ImGui::GetCursorScreenPos();
752
				auto boxColor = ImGui::ColorConvertFloat4ToU32(boundingBoxColor);
753
				auto drawBox = [&](int x, int y, int w, int h) {
754
					gl::vec2 tl = scrnPos + gl::vec2{float(x), float(y)} * float(zm);
755
					gl::vec2 br = tl + gl::vec2{float(w), float(h)} * float(zm);
756
					drawList->AddRect(tl, br, boxColor);
757
				};
758
759
				gl::vec2 topLeft = ImGui::GetCursorPos();
760
				if (checkerBoardSize) {
761
					ImGui::Image(checkerTex.getImGui(), fullSize,
762
						{}, fullSize / (4.0f * float(checkerBoardSize)));
763
				}
764
				ImGui::SetCursorPos(topLeft);
765
				ImGui::Image(renderTex.getImGui(), fullSize);
766
				bool hovered = ImGui::IsItemHovered();
767
				auto hoverPos = trunc((gl::vec2(ImGui::GetIO().MousePos) - scrnPos) / gl::vec2(float(zm)));
768
				ImGui::SameLine();
769
770
				im::Group([&]{
771
					ImGui::Checkbox("Bounding box", &drawBoundingBox);
772
					im::Indent([&]{
773
						im::Disabled(!drawBoundingBox, [&]{
774
							ImGui::ColorEdit4("color", boundingBoxColor.data(),
775
								ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_AlphaBar);
776
							ImGui::RadioButton("On hovered sprites", &boundingBoxOnAll, 0);
777
							ImGui::RadioButton("On all sprites", &boundingBoxOnAll, 1);
778
779
						});
780
					});
781
					{
782
						auto maxStr = tmpStrCat("Max ", spriteLimit, " sprites per line");
783
						ImGui::Checkbox(maxStr.c_str(), &enableLimitPerLine);
784
					}
785
					{
786
						auto stopStr = tmpStrCat("Stop at y=", stopY);
787
						ImGui::Checkbox(stopStr.c_str(), &enableStopY);
788
					}
789
790
					if (hovered) {
791
						ImGui::Separator();
792
						auto [hx, hy] = hoverPos;
793
						ImGui::StrCat("x,y=", hx, ',', hy, " (0x", hex_string<2>(hx), ",0x", hex_string<2>(hy), ')');
794
						ImGui::Spacing();
795
796
						for (int i : xrange(nrClippedBoxes)) {
797
							const auto& b = clippedBoxes[i];
798
							if ((b.x <= hx) && (hx < (b.x + b.w)) &&
799
							    (b.y <= hy) && (hy < (b.y + b.h))) {
800
								if (drawBoundingBox && (boundingBoxOnAll == 0)) {
801
									drawBox(b.x, b.y, b.w, b.h);
802
								}
803
								ImGui::Bullet();
804
								ImGui::StrCat("sprite ", b.sprite, ": pat=", b.pattern, " (0x", hex_string<2>(b.pattern), ")\n"
805
								              "x,y=", b.vramX, ',', b.vramY, " (0x", hex_string<2>(b.vramX), ",0x", hex_string<2>(b.vramY), ')');
806
							}
807
						}
808
					}
809
				});
810
811
				if (drawBoundingBox && (boundingBoxOnAll == 1)) {
812
					for (int i : xrange(nrClippedBoxes)) {
813
						const auto& b = clippedBoxes[i];
814
						drawBox(b.x, b.y, b.w, b.h);
815
					}
816
				}
817
			});
818
		});
819
820
	});
821
}
822
823
} // namespace openmsx