| 1 |
#include "ImGuiVdpRegs.hh" |
| 2 |
|
| 3 |
#include "ImGuiCpp.hh" |
| 4 |
#include "ImGuiUtils.hh" |
| 5 |
|
| 6 |
#include "VDP.hh" |
| 7 |
|
| 8 |
#include "strCat.hh" |
| 9 |
#include "xrange.hh" |
| 10 |
#include "zstring_view.hh" |
| 11 |
|
| 12 |
#include "imgui.h" |
| 13 |
#include "imgui_internal.h" |
| 14 |
|
| 15 |
#include <array> |
| 16 |
#include <bit> |
| 17 |
|
| 18 |
namespace openmsx { |
| 19 |
|
| 20 |
void ImGuiVdpRegs::save(ImGuiTextBuffer& buf) |
| 21 |
{ |
| 22 |
savePersistent(buf, *this, persistentElements); |
| 23 |
} |
| 24 |
|
| 25 |
void ImGuiVdpRegs::loadLine(std::string_view name, zstring_view value) |
| 26 |
{ |
| 27 |
loadOnePersistent(name, value, *this, persistentElements); |
| 28 |
} |
| 29 |
|
| 30 |
static zstring_view modeName(uint32_t v) |
| 31 |
{ |
| 32 |
|
| 33 |
static constexpr std::array<zstring_view, 32> modeNames = { |
| 34 |
"Graphic 1 (screen 1)", |
| 35 |
"Multicolor (screen 3)", |
| 36 |
"Text 1 (screen 0, width 40)", |
| 37 |
"3 - invalid", |
| 38 |
|
| 39 |
"Graphic 2 (screen 2)", |
| 40 |
"Multicolor-Q", |
| 41 |
"Text 1-Q", |
| 42 |
"7 - invalid", |
| 43 |
|
| 44 |
"Graphic 3 (screen 4)", |
| 45 |
"10 - invalid", |
| 46 |
"Text 2 (screen 0, width 80)", |
| 47 |
"11 - invalid", |
| 48 |
|
| 49 |
"Graphic 4 (screen 5)", |
| 50 |
"14 - invalid", |
| 51 |
"13 - invalid", |
| 52 |
"15 - invalid", |
| 53 |
|
| 54 |
"Graphic 5 (screen 6)", |
| 55 |
"18 - invalid", |
| 56 |
"17 - invalid", |
| 57 |
"19 - invalid", |
| 58 |
|
| 59 |
"Graphic 6 (screen 7)", |
| 60 |
"22 - invalid", |
| 61 |
"21 - invalid", |
| 62 |
"23 - invalid", |
| 63 |
|
| 64 |
"24 - invalid", |
| 65 |
"26 - invalid", |
| 66 |
"25 - invalid", |
| 67 |
"27 - invalid", |
| 68 |
|
| 69 |
"Graphic 7 (screen 8)", |
| 70 |
"30 - invalid", |
| 71 |
"29 - invalid", |
| 72 |
"31 - invalid", |
| 73 |
}; |
| 74 |
assert(v < 32); |
| 75 |
return modeNames[v]; |
| 76 |
} |
| 77 |
|
| 78 |
using Bits = std::array<const char*, 8>; |
| 79 |
struct RegisterDescription { |
| 80 |
Bits bits; |
| 81 |
std::string_view name; |
| 82 |
}; |
| 83 |
using RD = RegisterDescription; |
| 84 |
static constexpr auto registerDescriptions = std::array{ |
| 85 |
RD{{" 0 ", "DG ", "IE2", "IE1", "M5 ", "M4 ", "M3 ", " 0 "}, "Mode register 0"}, |
| 86 |
RD{{" 0 ", "BL ", "IE0", "M1 ", "M2 ", " 0 ", "SI ", "MAG"}, "Mode register 1"}, |
| 87 |
RD{{" 0 ", "A16", "A15", "A14", "A13", "A12", "A11", "A10"}, "Pattern name table base address register"}, |
| 88 |
RD{{"A13", "A12", "A11", "A10", "A9 ", "A8 ", "A7 ", "A6 "}, "Color table base address register low"}, |
| 89 |
RD{{" 0 ", " 0 ", "A16", "A15", "A14", "A13", "A12", "A11"}, "Pattern generator table base address register"}, |
| 90 |
RD{{"A14", "A13", "A12", "A11", "A10", "A9 ", "A8 ", "A7 "}, "Sprite attribute table base address register low"}, |
| 91 |
RD{{" 0 ", " 0 ", "A16", "A15", "A14", "A13", "A12", "A11"}, "Sprite pattern generator table base address register"}, |
| 92 |
RD{{"TC3", "TC2", "TC1", "TC0", "BD3", "BD2", "BD1", "BD0"}, "Text color/Back drop color register"}, |
| 93 |
|
| 94 |
RD{{"MS ", "LP ", "TP ", "CB ", "VR ", " 0 ", "SPD", "BW "}, "Mode register 2"}, |
| 95 |
RD{{"LN ", " 0 ", "S1 ", "S2 ", "IL ", "EO ", "*NT", "DC "}, "Mode register 3"}, |
| 96 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "A16", "A15", "A14"}, "Color table base address register high"}, |
| 97 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "A16", "A15"}, "Sprite attribute table base address register high"}, |
| 98 |
RD{{"T23", "T22", "T21", "T20", "BC3", "BC2", "BC1", "BC0"}, "Text color/Back color register"}, |
| 99 |
RD{{"ON3", "ON2", "ON1", "ON0", "OF3", "OF2", "OF1", "OF0"}, "Blinking period register"}, |
| 100 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "A16", "A15", "A14"}, "VRAM Access base address register"}, |
| 101 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", "S3 ", "S2 ", "S1 ", "S0 "}, "Status register pointer"}, |
| 102 |
|
| 103 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", "C3 ", "C2 ", "C1 ", "C0 "}, "Color palette address register"}, |
| 104 |
RD{{"AII", " 0 ", "RS5", "RS4", "RS3", "RS2", "RS1", "RS0"}, "Control register pointer"}, |
| 105 |
RD{{"V3 ", "V2 ", "V1 ", "V0 ", "H3 ", "H2 ", "H1 ", "H0 "}, "Display adjust register"}, |
| 106 |
RD{{"IL7", "IL6", "IL5", "IL4", "IL3", "IL2", "IL1", "IL0"}, "Interrupt line register"}, |
| 107 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 "}, "Color burst register 1"}, |
| 108 |
RD{{" 0 ", " 0 ", " 1 ", " 1 ", " 1 ", " 0 ", " 1 ", " 1 "}, "Color burst register 2"}, |
| 109 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 1 ", " 0 ", " 1 "}, "Color burst register 3"}, |
| 110 |
RD{{"DO7", "DO6", "DO5", "DO4", "DO3", "DO2", "DO1", "DO0"}, "Display offset register"}, |
| 111 |
|
| 112 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 113 |
RD{{" 0 ", "CMD", "VDS", "YAE", "YJK", "WTE", "MSK", "SP2"}, "V9958 control register"}, |
| 114 |
RD{{" 0 ", " 0 ", "HO8", "HO7", "HO6", "HO5", "HO4", "HO3"}, "Horizontal scroll register high"}, |
| 115 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "HO2", "HO1", "HO0"}, "Horizontal scroll register low"}, |
| 116 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 117 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 118 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 119 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 120 |
|
| 121 |
RD{{"SX7", "SX6", "SX5", "SX4", "SX3", "SX2", "SX1", "SX0"}, "Source X low register"}, |
| 122 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "SX8"}, "Source X high register"}, |
| 123 |
RD{{"SY7", "SY6", "SY5", "SY4", "SY3", "SY2", "SY1", "SY0"}, "Source Y low register"}, |
| 124 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "SY9", "SY8"}, "Source Y high register"}, |
| 125 |
RD{{"DX7", "DX6", "DX5", "DX4", "DX3", "DX2", "DX1", "DX0"}, "Destination X low register"}, |
| 126 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "DX8"}, "Destination X high register"}, |
| 127 |
RD{{"DY7", "DY6", "DY5", "DY4", "DY3", "DY2", "DY1", "DY0"}, "Destination Y low register"}, |
| 128 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "DY9", "DY8"}, "Destination Y high register"}, |
| 129 |
|
| 130 |
RD{{"NX7", "NX6", "NX5", "NX4", "NX3", "NX2", "NX1", "NX0"}, "Number of dots X low register"}, |
| 131 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "NX8"}, "Number of dots X high register"}, |
| 132 |
RD{{"NY7", "NY6", "NY5", "NY4", "NY3", "NY2", "NY1", "NY0"}, "Number of dots Y low register"}, |
| 133 |
RD{{" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "NY9", "NY8"}, "Number of dots Y high register"}, |
| 134 |
RD{{"CH3", "CH2", "CH1", "CH0", "CL3", "CL2", "CL1", "CL0"}, "Color register"}, |
| 135 |
RD{{" 0 ", "MXC", "MXD", "MXS", "DIY", "DIX", "EQ ", "MAJ"}, "Argument register"}, |
| 136 |
RD{{"CM3", "CM2", "CM1", "CM0", "LO3", "LO2", "LO1", "LO0"}, "Command register"}, |
| 137 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 138 |
|
| 139 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 140 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 141 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 142 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 143 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 144 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 145 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 146 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 147 |
|
| 148 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 149 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 150 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 151 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 152 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 153 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 154 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 155 |
RD{{" - ", " - ", " - ", " - ", " - ", " - ", " - ", " - "}, ""}, |
| 156 |
|
| 157 |
|
| 158 |
|
| 159 |
RD{{" F ", "5S ", " C ", "FS4", "FS3", "FS2", "FS1", "FS0"}, "Status register 0"}, |
| 160 |
RD{{"FL ", "LPS", "ID4", "ID3", "ID2", "ID1", "ID0", "FH "}, "Status register 1"}, |
| 161 |
RD{{"TR ", "VR ", "HR ", "BD ", " 1 ", " 1 ", "EO ", "CE "}, "Status register 2"}, |
| 162 |
RD{{"X7 ", "X6 ", "X5 ", "X4 ", "X3 ", "X2 ", "X1 ", "X0 "}, "Column register low"}, |
| 163 |
RD{{" 1 ", " 1 ", " 1 ", " 1 ", " 1 ", " 1 ", " 1 ", "X8 "}, "Column register high"}, |
| 164 |
RD{{"Y7 ", "Y6 ", "Y5 ", "Y4 ", "Y3 ", "Y2 ", "Y1 ", "Y0 "}, "Row register low"}, |
| 165 |
RD{{" 1 ", " 1 ", " 1 ", " 1 ", " 1 ", " 1 ", "EO ", "Y8 "}, "Row register high"}, |
| 166 |
RD{{"C7 ", "C6 ", "C5 ", "C4 ", "C3 ", "C2 ", "C1 ", "C0 "}, "Color register"}, |
| 167 |
|
| 168 |
RD{{"BX7", "BX6", "BX5", "BX4", "BX3", "BX2", "BX1", "BX0"}, "Border X register low"}, |
| 169 |
RD{{" 1 ", " 1 ", " 1 ", " 1 ", " 1 ", " 1 ", " 1 ", "BX8"}, "Border X register high"}, |
| 170 |
}; |
| 171 |
|
| 172 |
struct SubFunction { |
| 173 |
uint8_t reg = uint8_t(-1); |
| 174 |
uint8_t mask = 0; |
| 175 |
}; |
| 176 |
struct RegFunction { |
| 177 |
|
| 178 |
|
| 179 |
std::array<SubFunction, 2> subs; |
| 180 |
std::string_view toolTip; |
| 181 |
TemporaryString(*displayFunc)(uint32_t); |
| 182 |
}; |
| 183 |
using S = SubFunction; |
| 184 |
using R = RegFunction; |
| 185 |
using namespace std::literals; |
| 186 |
static TemporaryString noExplanation(uint32_t) { return TemporaryString{}; } |
| 187 |
static TemporaryString spacing(uint32_t) { return TemporaryString("\n"); } |
| 188 |
|
| 189 |
static const VDP* g_vdp; |
| 190 |
static std::array<uint8_t, 64 + 10> registerValues; |
| 191 |
static uint8_t getMode() { return uint8_t(((registerValues[1] & 0x18) >> 3) | ((registerValues[0] & 0x0E) << 1)); } |
| 192 |
static bool isText2Mode() { return getMode() == 0b01010; } |
| 193 |
static bool isGraph23Mode() { return getMode() == one_of(0b00100, 0b01000); } |
| 194 |
static bool isSprite2Mode() { return getMode() == one_of(0b01000, 0b01100, 0b10000, 0b10100, 0b11100); } |
| 195 |
static bool isBitmapMode() { return getMode() >= 0x0C; } |
| 196 |
static bool isPlanarMode() { return (getMode() & 0x14) == 0x14; } |
| 197 |
|
| 198 |
static constexpr auto regFunctions = std::array{ |
| 199 |
|
| 200 |
R{{S{0, 0}}, "", [](uint32_t) { return TemporaryString("screen: "); }}, |
| 201 |
R{{S{1, 0x40}}, "screen display enable/disable", |
| 202 |
[](uint32_t v) { return TemporaryString(v ? "enabled" : "disabled"); }}, |
| 203 |
R{{S{0, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 204 |
R{{S{1, 0x18}, S{0, 0x0E}}, "display mode", |
| 205 |
[](uint32_t v) { return TemporaryString(modeName(v)); }}, |
| 206 |
R{{S{9, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 207 |
R{{S{9, 0x02}}, "select PAL or NTSC", |
| 208 |
[](uint32_t v) { return TemporaryString(v ? "PAL (50Hz)" : "NTSC (60Hz)"); }}, |
| 209 |
R{{S{0, 0}}, "", [](uint32_t) { return TemporaryString("\n"); }}, |
| 210 |
|
| 211 |
R{{S{9, 0x80}}, "select 192 or 212 lines", |
| 212 |
[](uint32_t v) { return TemporaryString(v ? "212 lines" : "192 lines"); }}, |
| 213 |
R{{S{9, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 214 |
R{{S{9, 0x08}}, "interlace enable/disable", |
| 215 |
[](uint32_t v) { return TemporaryString(v ? "interlaced" : "non-interlaced"); }}, |
| 216 |
R{{S{9, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 217 |
R{{S{9, 0x04}}, "alternate odd/even pages enable/disable", |
| 218 |
[](uint32_t v) { return tmpStrCat(v ? ""sv : "do not"sv, " alternate odd/even pages\n"); }}, |
| 219 |
|
| 220 |
R{{S{1, 0}}, "", [](uint32_t) { return TemporaryString("sprites: "); }}, |
| 221 |
R{{S{8, 0x02}}, "sprite enable/disable", |
| 222 |
[](uint32_t v) { return TemporaryString(v ? "disabled" : "enabled"); }}, |
| 223 |
R{{S{8, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 224 |
R{{S{1, 0x02}}, "sprite size, 8x8 or 16x16", |
| 225 |
[](uint32_t v) { return tmpStrCat("size=", v ? "16x16"sv : "8x8"sv); }}, |
| 226 |
R{{S{1, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 227 |
R{{S{1, 0x01}}, "sprite magnification enable/disable", |
| 228 |
[](uint32_t v) { return TemporaryString(v ? "magnified\n" : "not-magnified\n"); }}, |
| 229 |
|
| 230 |
R{{S{8, 0x20}}, "color 0 transparent or not", |
| 231 |
[](uint32_t v) { return tmpStrCat("color 0 is ", v ? "a regular color\n"sv : "transparent\n"sv); }}, |
| 232 |
|
| 233 |
R{{S{1, 0x20}}, "V-Blank interrupt enable/disable", |
| 234 |
[](uint32_t v) { return tmpStrCat("V-Blank-IRQ: ", v ? "enabled"sv : "disabled"sv); }}, |
| 235 |
R{{S{1, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 236 |
R{{S{0, 0x10}}, "line interrupt enable/disable", |
| 237 |
[](uint32_t v) { return tmpStrCat("line-IRQ: ", v ? "enabled"sv : "disabled"sv, '\n'); }}, |
| 238 |
|
| 239 |
R{{S{0, 0x40}}, "digitize", &noExplanation}, |
| 240 |
R{{S{0, 0x20}}, "light pen interrupt enable", &noExplanation}, |
| 241 |
R{{S{1, 0x04}}, "fast blink mode", &noExplanation}, |
| 242 |
R{{S{8, 0x80}}, "mouse", &noExplanation}, |
| 243 |
R{{S{8, 0x40}}, "light pen", &noExplanation}, |
| 244 |
R{{S{8, 0x10}}, "set color bus input/output", &noExplanation}, |
| 245 |
R{{S{8, 0x08}}, "select VRAM type", &noExplanation}, |
| 246 |
R{{S{8, 0x01}}, "select black&white output", &noExplanation}, |
| 247 |
R{{S{9, 0x30}}, "simultaneous mode", &noExplanation}, |
| 248 |
R{{S{9, 0x01}}, "set DLCLK input/output", &noExplanation}, |
| 249 |
R{{S{0, 0}}, "", &spacing}, |
| 250 |
|
| 251 |
|
| 252 |
R{{S{2, 0x7F}}, "name table address", [](uint32_t v) { |
| 253 |
if (isText2Mode()) { |
| 254 |
v &= ~0x03; |
| 255 |
} else if (isBitmapMode()) { |
| 256 |
v &= ~0x1F; |
| 257 |
if (isPlanarMode()) { |
| 258 |
v *= 2; |
| 259 |
} |
| 260 |
} |
| 261 |
return tmpStrCat("name table: 0x", hex_string<5>(v << 10), '\n'); |
| 262 |
}}, |
| 263 |
R{{S{3, 0xFF}, S{10, 0x07}}, "color table address", [](uint32_t v) { |
| 264 |
if (isText2Mode()) v &= ~0x07; |
| 265 |
if (isGraph23Mode()) v &= ~0x7F; |
| 266 |
return tmpStrCat("color table: 0x", hex_string<5>(v << 6), '\n'); |
| 267 |
}}, |
| 268 |
R{{S{4, 0x3F}}, "pattern table address", [](uint32_t v) { |
| 269 |
if (isGraph23Mode()) v &= ~0x03; |
| 270 |
return tmpStrCat("pattern table: 0x", hex_string<5>(v << 11), '\n'); |
| 271 |
}}, |
| 272 |
R{{S{5, 0xFF}, S{11, 0x03}}, "sprite attribute table address", [](uint32_t v) { |
| 273 |
if (isSprite2Mode()) { |
| 274 |
v &= ~0x03; |
| 275 |
} |
| 276 |
return tmpStrCat("sprite attribute table: 0x", hex_string<5>(v << 7), '\n'); |
| 277 |
}}, |
| 278 |
R{{S{6, 0x3F}}, "sprite pattern table address", [](uint32_t v) { |
| 279 |
return tmpStrCat("sprite pattern table: 0x", hex_string<5>(v << 11), '\n'); |
| 280 |
}}, |
| 281 |
R{{S{2, 0}}, "", &spacing}, |
| 282 |
|
| 283 |
|
| 284 |
R{{S{13, 0}}, "", [](uint32_t) { return TemporaryString("text color:"); }}, |
| 285 |
R{{S{7, 0xF0}}, "text color", |
| 286 |
[](uint32_t v) { return tmpStrCat(" foreground=", v); }}, |
| 287 |
R{{S{7, 0x0F}}, "background color", |
| 288 |
[](uint32_t v) { return tmpStrCat(" background=", v, '\n'); }}, |
| 289 |
R{{S{13, 0}}, "", [](uint32_t) { return TemporaryString("text blink color:"); }}, |
| 290 |
R{{S{12, 0xF0}}, "text blink color", |
| 291 |
[](uint32_t v) { return tmpStrCat(" foreground=", v); }}, |
| 292 |
R{{S{12, 0x0F}}, "background blink color", |
| 293 |
[](uint32_t v) { return tmpStrCat(" background=", v, '\n'); }}, |
| 294 |
R{{S{13, 0}}, "", [](uint32_t) { return TemporaryString("blink period:"); }}, |
| 295 |
R{{S{13, 0xF0}}, "blink period on", |
| 296 |
[](uint32_t v) { return tmpStrCat(" on=", 10 * v); }}, |
| 297 |
R{{S{13, 0x0F}}, "blink period off", |
| 298 |
[](uint32_t v) { return tmpStrCat(" off=", 10 * v); }}, |
| 299 |
R{{S{13, 0}}, "", [](uint32_t) { return TemporaryString(" frames\n"); }}, |
| 300 |
R{{S{7, 0}}, "", &spacing}, |
| 301 |
|
| 302 |
|
| 303 |
R{{S{18, 0}}, "", [](uint32_t) { return TemporaryString("set-adjust:"); }}, |
| 304 |
R{{S{18, 0x0F}}, "horizontal set-adjust", |
| 305 |
[](uint32_t v) { return tmpStrCat(" horizontal=", int(v ^ 7) - 7); }}, |
| 306 |
R{{S{18, 0xF0}}, "vertical set-adjust", |
| 307 |
[](uint32_t v) { return tmpStrCat(" vertical=", int(v ^ 7) - 7, '\n'); }}, |
| 308 |
R{{S{19, 0xFF}}, "line number for line-interrupt", |
| 309 |
[](uint32_t v) { return tmpStrCat("line interrupt=", v, '\n'); }}, |
| 310 |
R{{S{23, 0xFF}}, "vertical scroll (line number for 1st line)", |
| 311 |
[](uint32_t v) { return tmpStrCat("vertical scroll=", v, '\n'); }}, |
| 312 |
R{{S{18, 0}}, "", &spacing}, |
| 313 |
|
| 314 |
|
| 315 |
R{{S{14, 0x07}}, "VRAM access base address", |
| 316 |
[](uint32_t v) { return tmpStrCat("VRAM access base address: ", hex_string<5>(v << 14)); }}, |
| 317 |
R{{S{14, 0}}, "", [](uint32_t) { |
| 318 |
auto addr = (registerValues[14] << 14) | g_vdp->getVramPointer(); |
| 319 |
return tmpStrCat(", full address: ", hex_string<5>(addr), '\n'); |
| 320 |
}}, |
| 321 |
R{{S{15, 0x0F}}, "select status register", |
| 322 |
[](uint32_t v) { return tmpStrCat("selected status register: ", v, '\n'); }}, |
| 323 |
R{{S{16, 0x0F}}, "select palette entry", |
| 324 |
[](uint32_t v) { return tmpStrCat("selected palette entry: ", v, '\n'); }}, |
| 325 |
R{{S{17, 0x3F}}, "indirect register access", |
| 326 |
[](uint32_t v) { return tmpStrCat("selected indirect register: ", v); }}, |
| 327 |
R{{S{17, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 328 |
R{{S{17, 0x80}}, "auto-increment", |
| 329 |
[](uint32_t v) { return tmpStrCat(v ? "no "sv : ""sv, "auto-increment\n"); }}, |
| 330 |
R{{S{14, 0}}, "", &spacing}, |
| 331 |
|
| 332 |
|
| 333 |
R{{S{27, 0x07}, S{26, 0x3F}}, "horizontal scroll", [](uint32_t v) { |
| 334 |
auto s = (v & ~7) - (v & 7); |
| 335 |
return tmpStrCat("horizontal scroll: ", s); |
| 336 |
}}, |
| 337 |
R{{S{25, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 338 |
R{{S{25, 0x01}}, "2-page scroll enable/disable", |
| 339 |
[](uint32_t v) { return tmpStrCat(v + 1, "-page"); }}, |
| 340 |
R{{S{25, 0}}, "", [](uint32_t) { return TemporaryString(", "); }}, |
| 341 |
R{{S{25, 0x02}}, "mask 8 left dots", |
| 342 |
[](uint32_t v) { return tmpStrCat(v ? ""sv : "not-"sv, "masked\n"); }}, |
| 343 |
R{{S{25, 0x18}}, "YJK/YAE mode", |
| 344 |
[](uint32_t v) { return tmpStrCat((v & 1) ? ((v & 2) ? "YJK+YAE"sv : "YJK"sv) : "RGB"sv, "-mode\n"); }}, |
| 345 |
R{{S{25, 0x40}}, "expand command mode", |
| 346 |
[](uint32_t v) { return tmpStrCat("commands ", (v ? "work in all "sv : "only work in bitmap-"sv), "modes\n"); }}, |
| 347 |
R{{S{25, 0x20}}, "output select between CPUCLK and /VDS", &noExplanation}, |
| 348 |
R{{S{25, 0x04}}, "enable/disable WAIT (not used on MSX)", &noExplanation}, |
| 349 |
R{{S{25, 0}}, "", &spacing}, |
| 350 |
|
| 351 |
|
| 352 |
R{{S{46, 0xF0}}, "command code", [](uint32_t v) { |
| 353 |
static constexpr std::array<zstring_view, 16> COMMANDS = { |
| 354 |
" ABRT"," ????"," ????"," ????","POINT"," PSET"," SRCH"," LINE", |
| 355 |
" LMMV"," LMMM"," LMCM"," LMMC"," HMMV"," HMMM"," YMMM"," HMMC" |
| 356 |
}; |
| 357 |
return TemporaryString(COMMANDS[v]); |
| 358 |
}}, |
| 359 |
R{{S{46, 0}}, "", [](uint32_t) { return TemporaryString("-"); }}, |
| 360 |
R{{S{46, 0x0F}}, "logical operation", [](uint32_t v) { |
| 361 |
static constexpr std::array<zstring_view, 16> OPS = { |
| 362 |
"IMP ","AND ","OR ","XOR ","NOT ","NOP ","NOP ","NOP ", |
| 363 |
"TIMP","TAND","TOR ","TXOR","TNOT","NOP ","NOP ","NOP " |
| 364 |
}; |
| 365 |
return TemporaryString(OPS[v]); |
| 366 |
}}, |
| 367 |
R{{S{32, 0}}, "", [](uint32_t) { return TemporaryString("("); }}, |
| 368 |
R{{S{32, 0xFF}, S{33, 0x01}}, "source X", [](uint32_t v) { return tmpStrCat(v); }}, |
| 369 |
R{{S{32, 0}}, "", [](uint32_t) { return TemporaryString(","); }}, |
| 370 |
R{{S{34, 0xFF}, S{35, 0x03}}, "source Y", [](uint32_t v) { return tmpStrCat(v); }}, |
| 371 |
R{{S{32, 0}}, "", [](uint32_t) { return TemporaryString(")->("); }}, |
| 372 |
R{{S{36, 0xFF}, S{37, 0x01}}, "destination X", [](uint32_t v) { return tmpStrCat(v); }}, |
| 373 |
R{{S{32, 0}}, "", [](uint32_t) { return TemporaryString(","); }}, |
| 374 |
R{{S{38, 0xFF}, S{39, 0x03}}, "destination Y", [](uint32_t v) { return tmpStrCat(v); }}, |
| 375 |
R{{S{32, 0}}, "", [](uint32_t) { return TemporaryString("),"); }}, |
| 376 |
R{{S{44, 0xFF}}, "color", [](uint32_t v) { return tmpStrCat(v); }}, |
| 377 |
R{{S{32, 0}}, "", [](uint32_t) { return TemporaryString(" ["); }}, |
| 378 |
R{{S{45, 0x04}}, "direction X", [](uint32_t v) { return TemporaryString(v ? "-" : "+"); }}, |
| 379 |
R{{S{40, 0xFF}, S{41, 0x01}}, "number of dots X", [](uint32_t v) { return tmpStrCat(v); }}, |
| 380 |
R{{S{32, 0}}, "", [](uint32_t) { return TemporaryString(","); }}, |
| 381 |
R{{S{45, 0x08}}, "direction Y", [](uint32_t v) { return TemporaryString(v ? "-" : "+"); }}, |
| 382 |
R{{S{42, 0xFF}, S{43, 0x03}}, "number of dots Y", [](uint32_t v) { return tmpStrCat(v); }}, |
| 383 |
R{{S{32, 0}}, "", [](uint32_t) { return TemporaryString("]\n"); }}, |
| 384 |
|
| 385 |
R{{S{45, 0x40}}, "expanded VRAM for CPU access", &noExplanation}, |
| 386 |
R{{S{45, 0x20}}, "expanded VRAM for destination", &noExplanation}, |
| 387 |
R{{S{45, 0x10}}, "expanded VRAM for source", &noExplanation}, |
| 388 |
R{{S{45, 0x02}}, "select equal/not-equal for SRCH command", &noExplanation}, |
| 389 |
R{{S{45, 0x01}}, "select major direction (X/Y) for LINE command", &noExplanation}, |
| 390 |
R{{S{32, 0}}, "", &spacing}, |
| 391 |
|
| 392 |
|
| 393 |
R{{S{64, 0x80}}, "VBLANK interrupt pending", |
| 394 |
[](uint32_t v) { return tmpStrCat("VBLANK-IRQ: ", v); }}, |
| 395 |
R{{S{65, 0}}, "", [](uint32_t) { return TemporaryString(" "); }}, |
| 396 |
R{{S{65, 0x01}}, "line interrupt pending", |
| 397 |
[](uint32_t v) { return tmpStrCat("line-IRQ: ", v); }}, |
| 398 |
R{{S{64, 0}}, "", [](uint32_t) { return TemporaryString("\n"); }}, |
| 399 |
|
| 400 |
R{{S{64, 0x40}}, "5th/9th sprite detected", |
| 401 |
[](uint32_t v) { return tmpStrCat(v ? ""sv : "no "sv, "5th/9th sprite per line"); }}, |
| 402 |
R{{S{64, 0x1F}}, "5th/9th sprite number", |
| 403 |
[](uint32_t v) { return tmpStrCat(" (sprite number=", v, ")\n"); }}, |
| 404 |
|
| 405 |
R{{S{64, 0x20}}, "sprite collision", |
| 406 |
[](uint32_t v) { return tmpStrCat(v ? ""sv : "no "sv, "sprite collision"); }}, |
| 407 |
R{{S{67, 0}}, "", [](uint32_t) { return TemporaryString(" (at coordinate "); }}, |
| 408 |
R{{S{67, 0xFF}, S{68, 0x01}}, "x-coordinate", |
| 409 |
[](uint32_t v) { return tmpStrCat("x=", v); }}, |
| 410 |
R{{S{69, 0xFF}, S{70, 0x03}}, "y-coordinate", |
| 411 |
[](uint32_t v) { return tmpStrCat("y=", v); }}, |
| 412 |
R{{S{67, 0}}, "", [](uint32_t) { return TemporaryString(")"); }}, |
| 413 |
R{{S{64, 0}}, "", [](uint32_t) { return TemporaryString("\n"); }}, |
| 414 |
|
| 415 |
R{{S{66, 0}}, "", [](uint32_t) { return TemporaryString("command: "); }}, |
| 416 |
R{{S{66, 0x01}}, "command executing", |
| 417 |
[](uint32_t v) { return tmpStrCat(v ? ""sv : "not "sv, "running"); }}, |
| 418 |
R{{S{66, 0}}, "", [](uint32_t) { return TemporaryString(" "); }}, |
| 419 |
R{{S{66, 0x80}}, "transfer ready flag", |
| 420 |
[](uint32_t v) { return tmpStrCat(v ? ""sv : "not "sv, "ready to transfer\n"); }}, |
| 421 |
|
| 422 |
R{{S{66, 0}}, "", [](uint32_t) { return TemporaryString("scan position: "); }}, |
| 423 |
R{{S{66, 0x20}}, "horizontal border", |
| 424 |
[](uint32_t v) { return tmpStrCat(v ? ""sv : "not "sv, "in horizontal border"); }}, |
| 425 |
R{{S{66, 0}}, "", [](uint32_t) { return TemporaryString(" "); }}, |
| 426 |
R{{S{66, 0x40}}, "vertical border", |
| 427 |
[](uint32_t v) { return tmpStrCat(v ? ""sv : "not "sv, "in vertical border\n"); }}, |
| 428 |
|
| 429 |
R{{S{65, 0x80}}, "light pen (not used on MSX)", &noExplanation}, |
| 430 |
R{{S{65, 0x40}}, "light pen switch (not used on MSX)", &noExplanation}, |
| 431 |
R{{S{65, 0x3E}}, "VDP identification (V9938 or V9958)", &noExplanation}, |
| 432 |
R{{S{66, 0x10}}, "SRCH command detected border color", &noExplanation}, |
| 433 |
R{{S{66, 0x02}}, "odd/even field", &noExplanation}, |
| 434 |
R{{S{71, 0xFF}}, "result of POINT command", &noExplanation}, |
| 435 |
R{{S{72, 0xFF}, S{73, 0x01}}, "result of SRCH command", &noExplanation}, |
| 436 |
}; |
| 437 |
|
| 438 |
static int lookupFunction(uint8_t reg, uint8_t mask) |
| 439 |
{ |
| 440 |
int i = 0; |
| 441 |
for (const auto& f : regFunctions) { |
| 442 |
for (const auto& s : f.subs) { |
| 443 |
if ((reg == s.reg) && (mask & s.mask)) { |
| 444 |
return i; |
| 445 |
} |
| 446 |
} |
| 447 |
++i; |
| 448 |
} |
| 449 |
return -1; |
| 450 |
} |
| 451 |
|
| 452 |
void ImGuiVdpRegs::drawSection(std::span<const uint8_t> showRegisters, std::span<const uint8_t> regValues, |
| 453 |
VDP& vdp, EmuTime time) |
| 454 |
{ |
| 455 |
ImGui::SameLine(); |
| 456 |
HelpMarker("Click to toggle bits, or edit the hex-field.\n" |
| 457 |
"Right-click to show/hide register explanation."); |
| 458 |
|
| 459 |
im::Table("table", 10, 0, [&]{ |
| 460 |
|
| 461 |
|
| 462 |
|
| 463 |
|
| 464 |
|
| 465 |
auto mouse_pos = ImGui::GetMousePos(); |
| 466 |
const auto* table = ImGui::GetCurrentTable(); |
| 467 |
auto isCellHovered = [&] { |
| 468 |
return ImGui::TableGetCellBgRect(table, table->CurrentColumn).Contains(mouse_pos); |
| 469 |
}; |
| 470 |
|
| 471 |
im::ID_for_range(showRegisters.size(), [&](int i) { |
| 472 |
auto reg = showRegisters[i]; |
| 473 |
|
| 474 |
|
| 475 |
const auto& rd = registerDescriptions[reg]; |
| 476 |
if (ImGui::TableNextColumn()) { |
| 477 |
auto name = tmpStrCat(reg < 64 ? "R#"sv : "S#"sv, reg < 64 ? reg : reg - 64); |
| 478 |
ImGui::AlignTextToFramePadding(); |
| 479 |
ImGui::TextUnformatted(static_cast<std::string_view>(name)); |
| 480 |
simpleToolTip(rd.name); |
| 481 |
} |
| 482 |
uint8_t value = regValues[reg]; |
| 483 |
bool writeReg = false; |
| 484 |
if (ImGui::TableNextColumn()) { |
| 485 |
if (reg < 64) { |
| 486 |
if (ImGui::InputScalar("##value", ImGuiDataType_U8, &value, nullptr, nullptr, "%02X", ImGuiInputTextFlags_CharsHexadecimal)) { |
| 487 |
writeReg = true; |
| 488 |
} |
| 489 |
} else { |
| 490 |
ImGui::Text("%02X", value); |
| 491 |
} |
| 492 |
} |
| 493 |
const auto& bits = rd.bits; |
| 494 |
im::ID_for_range(8, [&](int bit_) { |
| 495 |
if (ImGui::TableNextColumn()) { |
| 496 |
int bit = 7 - bit_; |
| 497 |
auto mask = narrow<uint8_t>(1 << bit); |
| 498 |
int f = lookupFunction(reg, mask); |
| 499 |
if (f != -1 && f == hoveredFunction) { |
| 500 |
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, |
| 501 |
getColor(imColor::YELLOW)); |
| 502 |
} |
| 503 |
im::StyleColor(ImGuiCol_Button, getColor(value & mask ? imColor::KEY_ACTIVE : imColor::KEY_NOT_ACTIVE), [&]{ |
| 504 |
if (ImGui::Button(bits[bit_], {40.0f, 0.0f})) { |
| 505 |
value ^= mask; |
| 506 |
writeReg = true; |
| 507 |
} |
| 508 |
}); |
| 509 |
if ((f != -1) && isCellHovered()) { |
| 510 |
newHoveredFunction = f; |
| 511 |
if (auto tip = regFunctions[f].toolTip; !tip.empty()) { |
| 512 |
simpleToolTip(tip); |
| 513 |
} |
| 514 |
} |
| 515 |
} |
| 516 |
}); |
| 517 |
if (writeReg && (reg < 64)) { |
| 518 |
vdp.changeRegister(reg, value, time); |
| 519 |
} |
| 520 |
}); |
| 521 |
}); |
| 522 |
if (explanation) { |
| 523 |
auto* drawList = ImGui::GetWindowDrawList(); |
| 524 |
gl::vec2 framePadding = ImGui::GetStyle().FramePadding; |
| 525 |
for (auto f : xrange(narrow<int>(regFunctions.size()))) { |
| 526 |
const auto& func = regFunctions[f]; |
| 527 |
if (!(contains(showRegisters, func.subs[0].reg) || |
| 528 |
contains(showRegisters, func.subs[1].reg))) { |
| 529 |
continue; |
| 530 |
} |
| 531 |
uint32_t value = 0; |
| 532 |
int shift = 0; |
| 533 |
for (const auto& sub : func.subs) { |
| 534 |
if (sub.reg == uint8_t(-1)) break; |
| 535 |
auto tmp = ((regValues[sub.reg] & sub.mask) >> std::countr_zero(sub.mask)); |
| 536 |
value |= tmp << shift; |
| 537 |
shift += std::popcount(sub.mask); |
| 538 |
} |
| 539 |
if (auto s = func.displayFunc(value); !s.empty()) { |
| 540 |
auto textColor = getColor(imColor::TEXT); |
| 541 |
bool canHover = func.subs[0].mask != 0; |
| 542 |
if (canHover && (f == hoveredFunction)) { |
| 543 |
gl::vec2 textSize = ImGui::CalcTextSize(s); |
| 544 |
gl::vec2 pos = ImGui::GetCursorScreenPos(); |
| 545 |
textColor = getColor(imColor::BLACK); |
| 546 |
drawList->AddRectFilled(pos - framePadding, pos + textSize + 2.0f * framePadding, getColor(imColor::YELLOW)); |
| 547 |
} |
| 548 |
im::StyleColor(ImGuiCol_Text, textColor, [&]{ |
| 549 |
ImGui::TextUnformatted(s); |
| 550 |
}); |
| 551 |
if (canHover) { |
| 552 |
if (ImGui::IsItemHovered()) { |
| 553 |
newHoveredFunction = f; |
| 554 |
} |
| 555 |
if (shift == 1) { |
| 556 |
if (auto reg = func.subs[0].reg; reg < 64) { |
| 557 |
if (ImGui::IsItemClicked()) { |
| 558 |
auto mask = func.subs[0].mask; |
| 559 |
vdp.changeRegister(reg, regValues[reg] ^ mask, time); |
| 560 |
} |
| 561 |
} |
| 562 |
} |
| 563 |
} |
| 564 |
if (!s.ends_with('\n')) ImGui::SameLine(); |
| 565 |
} |
| 566 |
} |
| 567 |
} |
| 568 |
} |
| 569 |
|
| 570 |
void ImGuiVdpRegs::paint(MSXMotherBoard* motherBoard) |
| 571 |
{ |
| 572 |
if (!show || !motherBoard) return; |
| 573 |
|
| 574 |
ImGui::SetNextWindowSize(gl::vec2{41, 29} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver); |
| 575 |
im::Window("VDP registers", &show, [&]{ |
| 576 |
auto* vdp = dynamic_cast<VDP*>(motherBoard->findDevice("VDP")); |
| 577 |
if (!vdp) return; |
| 578 |
auto time = motherBoard->getCurrentTime(); |
| 579 |
const bool tms99x8 = vdp->isMSX1VDP(); |
| 580 |
const bool v9958 = vdp->hasYJK(); |
| 581 |
|
| 582 |
g_vdp = vdp; |
| 583 |
for (auto reg : xrange(64)) { |
| 584 |
registerValues[reg] = vdp->peekRegister(reg, time); |
| 585 |
} |
| 586 |
for (auto reg : xrange(uint8_t(10))) { |
| 587 |
registerValues[reg + 64] = vdp->peekStatusReg(reg, time); |
| 588 |
} |
| 589 |
|
| 590 |
newHoveredFunction = -1; |
| 591 |
auto make_span = [](const auto& array) { |
| 592 |
return std::span<const uint8_t, std::dynamic_extent>(std::span(array)); |
| 593 |
}; |
| 594 |
im::TreeNode("Control registers", &openControl, [&]{ |
| 595 |
im::TreeNode("Mode registers", &openMode, [&]{ |
| 596 |
static constexpr auto modeRegs1 = std::to_array<uint8_t>({0, 1}); |
| 597 |
static constexpr auto modeRegs2 = std::to_array<uint8_t>({0, 1, 8, 9}); |
| 598 |
auto modeRegs = tms99x8 ? make_span(modeRegs1) : make_span(modeRegs2); |
| 599 |
drawSection(modeRegs, registerValues, *vdp, time); |
| 600 |
}); |
| 601 |
im::TreeNode("Table base registers", &openBase, [&]{ |
| 602 |
static constexpr auto tableRegs1 = std::to_array<uint8_t>({2, 3, 4, 5, 6}); |
| 603 |
static constexpr auto tableRegs2 = std::to_array<uint8_t>({2, 3, 10, 4, 5, 11, 6}); |
| 604 |
auto tableRegs = tms99x8 ? make_span(tableRegs1) : make_span(tableRegs2); |
| 605 |
drawSection(tableRegs, registerValues, *vdp, time); |
| 606 |
}); |
| 607 |
im::TreeNode("Color registers", &openColor, [&]{ |
| 608 |
static constexpr auto colorRegs1 = std::to_array<uint8_t>({7}); |
| 609 |
static constexpr auto colorRegs2 = std::to_array<uint8_t>({7, 12, 13, 20, 21, 22}); |
| 610 |
auto colorRegs = tms99x8 ? make_span(colorRegs1) : make_span(colorRegs2); |
| 611 |
drawSection(colorRegs, registerValues, *vdp, time); |
| 612 |
}); |
| 613 |
if (!tms99x8) { |
| 614 |
im::TreeNode("Display registers", &openDisplay, [&]{ |
| 615 |
static constexpr auto displayRegs = std::to_array<uint8_t>({18, 19, 23}); |
| 616 |
drawSection(displayRegs, registerValues, *vdp, time); |
| 617 |
}); |
| 618 |
} |
| 619 |
im::TreeNode("Access registers", &openAccess, [&]{ |
| 620 |
if (tms99x8) { |
| 621 |
ImGui::StrCat("VRAM access address: 0x", |
| 622 |
hex_string<4>(vdp->getVramPointer()), '\n'); |
| 623 |
} else { |
| 624 |
static constexpr auto accessRegs = std::to_array<uint8_t>({14, 15, 16, 17}); |
| 625 |
drawSection(accessRegs, registerValues, *vdp, time); |
| 626 |
} |
| 627 |
}); |
| 628 |
if (v9958) { |
| 629 |
im::TreeNode("V9958 registers", &openV9958, [&]{ |
| 630 |
static constexpr auto v9958Regs = std::to_array<uint8_t>({25, 26, 27}); |
| 631 |
drawSection(v9958Regs, registerValues, *vdp, time); |
| 632 |
}); |
| 633 |
} |
| 634 |
}); |
| 635 |
if (!tms99x8) { |
| 636 |
im::TreeNode("Command registers", &openCommand, [&]{ |
| 637 |
static constexpr auto cmdRegs = std::to_array<uint8_t>({ |
| 638 |
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46 |
| 639 |
}); |
| 640 |
drawSection(cmdRegs, registerValues, *vdp, time); |
| 641 |
}); |
| 642 |
} |
| 643 |
im::TreeNode("Status registers", &openStatus, [&]{ |
| 644 |
static constexpr auto statusRegs1 = std::to_array<uint8_t>({64}); |
| 645 |
static constexpr auto statusRegs2 = std::to_array<uint8_t>({ |
| 646 |
64, 65, 66, 67, 68, 69, 70, 71, 72, 73 |
| 647 |
}); |
| 648 |
auto statusRegs = tms99x8 ? make_span(statusRegs1) : make_span(statusRegs2); |
| 649 |
drawSection(statusRegs, registerValues, *vdp, time); |
| 650 |
}); |
| 651 |
hoveredFunction = newHoveredFunction; |
| 652 |
|
| 653 |
if (ImGui::IsWindowHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { |
| 654 |
ImGui::OpenPopup("context"); |
| 655 |
} |
| 656 |
im::Popup("context", [&]{ |
| 657 |
if (ImGui::Checkbox("Show explanation ", &explanation)) { |
| 658 |
ImGui::CloseCurrentPopup(); |
| 659 |
} |
| 660 |
}); |
| 661 |
}); |
| 662 |
} |
| 663 |
|
| 664 |
} |