src/platform/qt/IOViewer.cpp (view raw)
1/* Copyright (c) 2013-2015 Jeffrey Pfau
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6#include "IOViewer.h"
7
8#include "GameController.h"
9
10#include <QFontDatabase>
11#include <QGridLayout>
12#include <QRadioButton>
13#include <QSpinBox>
14
15extern "C" {
16#include "gba/io.h"
17}
18
19using namespace QGBA;
20
21
22QList<IOViewer::RegisterDescription> IOViewer::s_registers;
23
24const QList<IOViewer::RegisterDescription>& IOViewer::registerDescriptions() {
25 if (!s_registers.isEmpty()) {
26 return s_registers;
27 }
28 // 0x04000000: DISPCNT
29 s_registers.append({
30 { tr("Background mode"), 0, 3, {
31 tr("Mode 0: 4 tile layers"),
32 tr("Mode 1: 2 tile layers + 1 rotated/scaled tile layer"),
33 tr("Mode 2: 2 rotated/scaled tile layers"),
34 tr("Mode 3: Full 15-bit bitmap"),
35 tr("Mode 4: Full 8-bit bitmap"),
36 tr("Mode 5: Small 15-bit bitmap"),
37 QString(),
38 QString()
39 } },
40 { tr("CGB Mode"), 3, 1, true },
41 { tr("Frame select"), 4 },
42 { tr("Unlocked HBlank"), 5 },
43 { tr("Linear OBJ tile mapping"), 6 },
44 { tr("Force blank screen"), 7 },
45 { tr("Enable background 0"), 8 },
46 { tr("Enable background 1"), 9 },
47 { tr("Enable background 2"), 10 },
48 { tr("Enable background 3"), 11 },
49 { tr("Enable OBJ"), 12 },
50 { tr("Enable Window 0"), 13 },
51 { tr("Enable Window 1"), 14 },
52 { tr("Enable OBJ Window"), 15 },
53 });
54 // 0x04000002: Green swap (undocumented and unimplemented)
55 s_registers.append(RegisterDescription());
56 // 0x04000004: DISPSTAT
57 s_registers.append({
58 { tr("Currently in VBlank"), 0, 1, true },
59 { tr("Currently in HBlank"), 1, 1, true },
60 { tr("Currently in VCounter"), 2, 1, true },
61 { tr("Enable VBlank IRQ generation"), 3 },
62 { tr("Enable HBlank IRQ generation"), 4 },
63 { tr("Enable VCounter IRQ generation"), 5 },
64 { tr("VCounter scanline"), 8, 8 },
65 });
66 // 0x04000006: VCOUNT
67 s_registers.append({
68 { tr("Current scanline"), 0, 8, true },
69 });
70 // 0x04000008: BG0CNT
71 s_registers.append({
72 { tr("Priority"), 0, 2 },
73 { tr("Tile data base (* 16kB)"), 2, 2 },
74 { tr("Enable mosaic"), 6 },
75 { tr("Enable 256-color"), 7 },
76 { tr("Tile map base (* 2kB)"), 8, 5 },
77 { tr("Background dimensions"), 14, 2 },
78 });
79 // 0x0400000A: BG1CNT
80 s_registers.append({
81 { tr("Priority"), 0, 2 },
82 { tr("Tile data base (* 16kB)"), 2, 2 },
83 { tr("Enable mosaic"), 6 },
84 { tr("Enable 256-color"), 7 },
85 { tr("Tile map base (* 2kB)"), 8, 5 },
86 { tr("Background dimensions"), 14, 2 },
87 });
88 // 0x0400000C: BG2CNT
89 s_registers.append({
90 { tr("Priority"), 0, 2 },
91 { tr("Tile data base (* 16kB)"), 2, 2 },
92 { tr("Enable mosaic"), 6 },
93 { tr("Enable 256-color"), 7 },
94 { tr("Tile map base (* 2kB)"), 8, 5 },
95 { tr("Overflow wraps"), 13 },
96 { tr("Background dimensions"), 14, 2 },
97 });
98 // 0x0400000E: BG3CNT
99 s_registers.append({
100 { tr("Priority"), 0, 2 },
101 { tr("Tile data base (* 16kB)"), 2, 2 },
102 { tr("Enable mosaic"), 6 },
103 { tr("Enable 256-color"), 7 },
104 { tr("Tile map base (* 2kB)"), 8, 5 },
105 { tr("Overflow wraps"), 13 },
106 { tr("Background dimensions"), 14, 2 },
107 });
108 // 0x04000010: BG0HOFS
109 s_registers.append({
110 { tr("Horizontal offset"), 0, 9 },
111 });
112 // 0x04000012: BG0VOFS
113 s_registers.append({
114 { tr("Vertical offset"), 0, 9 },
115 });
116 // 0x04000014: BG1HOFS
117 s_registers.append({
118 { tr("Horizontal offset"), 0, 9 },
119 });
120 // 0x04000016: BG1VOFS
121 s_registers.append({
122 { tr("Vertical offset"), 0, 9 },
123 });
124 // 0x04000018: BG2HOFS
125 s_registers.append({
126 { tr("Horizontal offset"), 0, 9 },
127 });
128 // 0x0400001A: BG2VOFS
129 s_registers.append({
130 { tr("Vertical offset"), 0, 9 },
131 });
132 // 0x0400001C: BG3HOFS
133 s_registers.append({
134 { tr("Horizontal offset"), 0, 9 },
135 });
136 // 0x0400001E: BG3VOFS
137 s_registers.append({
138 { tr("Vertical offset"), 0, 9 },
139 });
140 // 0x04000020: BG2PA
141 s_registers.append({
142 { tr("Fractional part"), 0, 8 },
143 { tr("Integer part"), 8, 8 },
144 });
145 // 0x04000022: BG2PB
146 s_registers.append({
147 { tr("Fractional part"), 0, 8 },
148 { tr("Integer part"), 8, 8 },
149 });
150 // 0x04000024: BG2PC
151 s_registers.append({
152 { tr("Fractional part"), 0, 8 },
153 { tr("Integer part"), 8, 8 },
154 });
155 // 0x04000026: BG2PD
156 s_registers.append({
157 { tr("Fractional part"), 0, 8 },
158 { tr("Integer part"), 8, 8 },
159 });
160 // 0x04000028: BG2X_LO
161 s_registers.append({
162 { tr("Fractional part"), 0, 8 },
163 { tr("Integer part (bottom)"), 8, 8 },
164 });
165 // 0x0400002A: BG2X_HI
166 s_registers.append({
167 { tr("Integer part (top)"), 0, 12 },
168 });
169 // 0x0400002C: BG2Y_LO
170 s_registers.append({
171 { tr("Fractional part"), 0, 8 },
172 { tr("Integer part (bottom)"), 8, 8 },
173 });
174 // 0x0400002E: BG2Y_HI
175 s_registers.append({
176 { tr("Integer part (top)"), 0, 12 },
177 });
178 // 0x04000030: BG3PA
179 s_registers.append({
180 { tr("Fractional part"), 0, 8 },
181 { tr("Integer part"), 8, 8 },
182 });
183 // 0x04000032: BG3PB
184 s_registers.append({
185 { tr("Fractional part"), 0, 8 },
186 { tr("Integer part"), 8, 8 },
187 });
188 // 0x04000034: BG3PC
189 s_registers.append({
190 { tr("Fractional part"), 0, 8 },
191 { tr("Integer part"), 8, 8 },
192 });
193 // 0x04000036: BG3PD
194 s_registers.append({
195 { tr("Fractional part"), 0, 8 },
196 { tr("Integer part"), 8, 8 },
197 });
198 // 0x04000038: BG3X_LO
199 s_registers.append({
200 { tr("Fractional part"), 0, 8 },
201 { tr("Integer part (bottom)"), 8, 8 },
202 });
203 // 0x0400003A: BG3X_HI
204 s_registers.append({
205 { tr("Integer part (top)"), 0, 12 },
206 });
207 // 0x0400003C: BG3Y_LO
208 s_registers.append({
209 { tr("Fractional part"), 0, 8 },
210 { tr("Integer part (bottom)"), 8, 8 },
211 });
212 // 0x0400003E: BG3Y_HI
213 s_registers.append({
214 { tr("Integer part (top)"), 0, 12 },
215 });
216 // 0x04000040: WIN0H
217 s_registers.append({
218 { tr("End x"), 0, 8 },
219 { tr("Start x"), 8, 8 },
220 });
221 // 0x04000042: WIN1H
222 s_registers.append({
223 { tr("End x"), 0, 8 },
224 { tr("Start x"), 8, 8 },
225 });
226 // 0x04000044: WIN0V
227 s_registers.append({
228 { tr("End y"), 0, 8 },
229 { tr("Start y"), 8, 8 },
230 });
231 // 0x04000046: WIN1V
232 s_registers.append({
233 { tr("End y"), 0, 8 },
234 { tr("Start y"), 8, 8 },
235 });
236 // 0x04000048: WININ
237 s_registers.append({
238 { tr("Window 0 enable BG 0"), 0 },
239 { tr("Window 0 enable BG 1"), 1 },
240 { tr("Window 0 enable BG 2"), 2 },
241 { tr("Window 0 enable BG 3"), 3 },
242 { tr("Window 0 enable OBJ"), 4 },
243 { tr("Window 0 enable blend"), 5 },
244 { tr("Window 1 enable BG 0"), 8 },
245 { tr("Window 1 enable BG 1"), 9 },
246 { tr("Window 1 enable BG 2"), 10 },
247 { tr("Window 1 enable BG 3"), 11 },
248 { tr("Window 1 enable OBJ"), 12 },
249 { tr("Window 1 enable blend"), 13 },
250 });
251 // 0x0400004A: WINOUT
252 s_registers.append({
253 { tr("Outside window enable BG 0"), 0 },
254 { tr("Outside window enable BG 1"), 1 },
255 { tr("Outside window enable BG 2"), 2 },
256 { tr("Outside window enable BG 3"), 3 },
257 { tr("Outside window enable OBJ"), 4 },
258 { tr("Outside window enable blend"), 5 },
259 { tr("OBJ window enable BG 0"), 8 },
260 { tr("OBJ window enable BG 1"), 9 },
261 { tr("OBJ window enable BG 2"), 10 },
262 { tr("OBJ window enable BG 3"), 11 },
263 { tr("OBJ window enable OBJ"), 12 },
264 { tr("OBJ window enable blend"), 13 },
265 });
266 // 0x0400004C: MOSAIC
267 s_registers.append({
268 { tr("Background mosaic size vertical"), 0, 4 },
269 { tr("Background mosaic size horizontal"), 4, 4 },
270 { tr("Object mosaic size vertical"), 8, 4 },
271 { tr("Object mosaic size horizontal"), 12, 4 },
272 });
273 // 0x0400004E: Unused
274 s_registers.append(RegisterDescription());
275 // 0x04000050: BLDCNT
276 s_registers.append({
277 { tr("BG 0 target 1"), 0 },
278 { tr("BG 1 target 1"), 1 },
279 { tr("BG 2 target 1"), 2 },
280 { tr("BG 3 target 1"), 3 },
281 { tr("OBJ target 1"), 4 },
282 { tr("Backdrop target 1"), 5 },
283 { tr("Blend mode"), 6, 2, {
284 tr("Disabled"),
285 tr("Additive blending"),
286 tr("Brighten"),
287 tr("Darken"),
288 } },
289 { tr("BG 0 target 2"), 8 },
290 { tr("BG 1 target 2"), 9 },
291 { tr("BG 2 target 2"), 10 },
292 { tr("BG 3 target 2"), 11 },
293 { tr("OBJ target 2"), 12 },
294 { tr("Backdrop target 2"), 13 },
295 });
296 // 0x04000052: BLDALPHA
297 s_registers.append({
298 { tr("Blend A (target 1)"), 0, 5 },
299 { tr("Blend B (target 2)"), 8, 5 },
300 });
301 // 0x04000054: BLDY
302 s_registers.append({
303 { tr("Blend Y"), 0, 5 },
304 });
305 return s_registers;
306}
307
308IOViewer::IOViewer(GameController* controller, QWidget* parent)
309 : QDialog(parent)
310 , m_controller(controller)
311{
312 m_ui.setupUi(this);
313
314 for (unsigned i = 0; i < REG_MAX >> 1; ++i) {
315 const char* reg = GBAIORegisterNames[i];
316 if (!reg) {
317 continue;
318 }
319 m_ui.regSelect->addItem("0x0400" + QString("%1: %2").arg(i << 1, 4, 16, QChar('0')).toUpper().arg(reg), i << 1);
320 }
321
322 const QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
323 m_ui.regValue->setFont(font);
324
325 connect(m_ui.buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonPressed(QAbstractButton*)));
326 connect(m_ui.buttonBox, SIGNAL(rejected()), this, SLOT(close()));
327 connect(m_ui.regSelect, SIGNAL(currentIndexChanged(int)), this, SLOT(selectRegister()));
328
329 m_b[0] = m_ui.b0;
330 m_b[1] = m_ui.b1;
331 m_b[2] = m_ui.b2;
332 m_b[3] = m_ui.b3;
333 m_b[4] = m_ui.b4;
334 m_b[5] = m_ui.b5;
335 m_b[6] = m_ui.b6;
336 m_b[7] = m_ui.b7;
337 m_b[8] = m_ui.b8;
338 m_b[9] = m_ui.b9;
339 m_b[10] = m_ui.bA;
340 m_b[11] = m_ui.bB;
341 m_b[12] = m_ui.bC;
342 m_b[13] = m_ui.bD;
343 m_b[14] = m_ui.bE;
344 m_b[15] = m_ui.bF;
345
346 for (int i = 0; i < 16; ++i) {
347 connect(m_b[i], SIGNAL(toggled(bool)), this, SLOT(bitFlipped()));
348 }
349
350 selectRegister(0);
351}
352
353void IOViewer::updateRegister() {
354 m_value = 0;
355 uint16_t value = 0;
356 m_controller->threadInterrupt();
357 if (m_controller->isLoaded()) {
358 value = GBAIORead(m_controller->thread()->gba, m_register);
359 }
360 m_controller->threadContinue();
361
362 for (int i = 0; i < 16; ++i) {
363 m_b[i]->setChecked(value & (1 << i) ? Qt::Checked : Qt::Unchecked);
364 }
365 m_value = value;
366 emit valueChanged();
367}
368
369void IOViewer::bitFlipped() {
370 m_value = 0;
371 for (int i = 0; i < 16; ++i) {
372 m_value |= m_b[i]->isChecked() << i;
373 }
374 m_ui.regValue->setText("0x" + QString("%1").arg(m_value, 4, 16, QChar('0')).toUpper());
375 emit valueChanged();
376}
377
378void IOViewer::writeback() {
379 m_controller->threadInterrupt();
380 if (m_controller->isLoaded()) {
381 GBAIOWrite(m_controller->thread()->gba, m_register, m_value);
382 }
383 m_controller->threadContinue();
384 updateRegister();
385}
386
387void IOViewer::selectRegister(unsigned address) {
388 m_register = address;
389 QGridLayout* box = static_cast<QGridLayout*>(m_ui.regDescription->layout());
390 if (box) {
391 // I can't believe there isn't a real way to do this...
392 while (!box->isEmpty()) {
393 QLayoutItem* item = box->takeAt(0);
394 if (item->widget()) {
395 delete item->widget();
396 }
397 delete item;
398 }
399 } else {
400 box = new QGridLayout;
401 }
402 if (registerDescriptions().count() > address >> 1) {
403 // TODO: Remove the check when done filling in register information
404 const RegisterDescription& description = registerDescriptions().at(address >> 1);
405 int i = 0;
406 for (const RegisterItem& ri : description) {
407 QLabel* label = new QLabel(ri.description);
408 box->addWidget(label, i, 0);
409 if (ri.size == 1) {
410 QCheckBox* check = new QCheckBox;
411 check->setEnabled(!ri.readonly);
412 box->addWidget(check, i, 1, Qt::AlignRight);
413 connect(check, SIGNAL(toggled(bool)), m_b[ri.start], SLOT(setChecked(bool)));
414 connect(m_b[ri.start], SIGNAL(toggled(bool)), check, SLOT(setChecked(bool)));
415 } else if (ri.items.empty()) {
416 QSpinBox* sbox = new QSpinBox;
417 sbox->setEnabled(!ri.readonly);
418 sbox->setMaximum((1 << ri.size) - 1);
419 box->addWidget(sbox, i, 1, Qt::AlignRight);
420 auto connection = connect(this, &IOViewer::valueChanged, [sbox, &ri, this]() {
421 int v = (m_value >> ri.start) & ((1 << ri.size) - 1);
422 bool signalsBlocked = sbox->blockSignals(true);
423 sbox->setValue(v);
424 sbox->blockSignals(signalsBlocked);
425 });
426 connect(sbox, &QObject::destroyed, [connection, this]() {
427 this->disconnect(connection);
428 });
429 } else {
430 QButtonGroup* group = new QButtonGroup(box);
431 group->setExclusive(true);
432 for (int o = 0; o < 1 << ri.size; ++o) {
433 if (ri.items.at(o).isNull()) {
434 continue;
435 }
436 ++i;
437 QRadioButton* button = new QRadioButton(ri.items.at(o));
438 button->setEnabled(!ri.readonly);
439 box->addWidget(button, i, 0, 1, 2, Qt::AlignLeft);
440 group->addButton(button, o);
441 }
442
443 auto connection = connect(this, &IOViewer::valueChanged, [group, this, &ri]() {
444 unsigned v = (m_value >> ri.start) & ((1 << ri.size) - 1);
445 for (int i = 0; i < 1 << ri.size; ++i) {
446 QAbstractButton* button = group->button(i);
447 if (!button) {
448 continue;
449 }
450 bool signalsBlocked = button->blockSignals(true);
451 button->setChecked(i == v);
452 button->blockSignals(signalsBlocked);
453 }
454 });
455 connect(group, &QObject::destroyed, [connection, this]() {
456 this->disconnect(connection);
457 });
458
459 }
460 ++i;
461 }
462 }
463 m_ui.regDescription->setLayout(box);
464 updateRegister();
465}
466
467void IOViewer::selectRegister() {
468 selectRegister(m_ui.regSelect->currentData().toUInt());
469}
470
471void IOViewer::buttonPressed(QAbstractButton* button) {
472 switch (m_ui.buttonBox->standardButton(button)) {
473 case QDialogButtonBox::Reset:
474 updateRegister();
475 break;
476 case QDialogButtonBox::Apply:
477 writeback();
478 break;
479 default:
480 break;
481 }
482}