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 return s_registers;
141}
142
143IOViewer::IOViewer(GameController* controller, QWidget* parent)
144 : QDialog(parent)
145 , m_controller(controller)
146{
147 m_ui.setupUi(this);
148
149 for (unsigned i = 0; i < REG_MAX >> 1; ++i) {
150 const char* reg = GBAIORegisterNames[i];
151 if (!reg) {
152 continue;
153 }
154 m_ui.regSelect->addItem("0x0400" + QString("%1: %2").arg(i << 1, 4, 16, QChar('0')).toUpper().arg(reg), i << 1);
155 }
156
157 const QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
158 m_ui.regValue->setFont(font);
159
160 connect(m_ui.buttonBox, SIGNAL(clicked(QAbstractButton*)), this, SLOT(buttonPressed(QAbstractButton*)));
161 connect(m_ui.buttonBox, SIGNAL(rejected()), this, SLOT(close()));
162 connect(m_ui.regSelect, SIGNAL(currentIndexChanged(int)), this, SLOT(selectRegister()));
163
164 m_b[0] = m_ui.b0;
165 m_b[1] = m_ui.b1;
166 m_b[2] = m_ui.b2;
167 m_b[3] = m_ui.b3;
168 m_b[4] = m_ui.b4;
169 m_b[5] = m_ui.b5;
170 m_b[6] = m_ui.b6;
171 m_b[7] = m_ui.b7;
172 m_b[8] = m_ui.b8;
173 m_b[9] = m_ui.b9;
174 m_b[10] = m_ui.bA;
175 m_b[11] = m_ui.bB;
176 m_b[12] = m_ui.bC;
177 m_b[13] = m_ui.bD;
178 m_b[14] = m_ui.bE;
179 m_b[15] = m_ui.bF;
180
181 for (int i = 0; i < 16; ++i) {
182 connect(m_b[i], SIGNAL(toggled(bool)), this, SLOT(bitFlipped()));
183 }
184
185 selectRegister(0);
186}
187
188void IOViewer::updateRegister() {
189 m_value = 0;
190 uint16_t value = 0;
191 m_controller->threadInterrupt();
192 if (m_controller->isLoaded()) {
193 value = GBAIORead(m_controller->thread()->gba, m_register);
194 }
195 m_controller->threadContinue();
196
197 for (int i = 0; i < 16; ++i) {
198 m_b[i]->setChecked(value & (1 << i) ? Qt::Checked : Qt::Unchecked);
199 }
200 m_value = value;
201}
202
203void IOViewer::bitFlipped() {
204 m_value = 0;
205 for (int i = 0; i < 16; ++i) {
206 m_value |= m_b[i]->isChecked() << i;
207 }
208 m_ui.regValue->setText("0x" + QString("%1").arg(m_value, 4, 16, QChar('0')).toUpper());
209}
210
211void IOViewer::writeback() {
212 m_controller->threadInterrupt();
213 if (m_controller->isLoaded()) {
214 GBAIOWrite(m_controller->thread()->gba, m_register, m_value);
215 }
216 m_controller->threadContinue();
217 updateRegister();
218}
219
220void IOViewer::selectRegister(unsigned address) {
221 m_register = address;
222 QGridLayout* box = static_cast<QGridLayout*>(m_ui.regDescription->layout());
223 if (box) {
224 // I can't believe there isn't a real way to do this...
225 while (!box->isEmpty()) {
226 QLayoutItem* item = box->takeAt(0);
227 if (item->widget()) {
228 delete item->widget();
229 }
230 delete item;
231 }
232 } else {
233 box = new QGridLayout;
234 }
235 if (registerDescriptions().count() > address >> 1) {
236 // TODO: Remove the check when done filling in register information
237 const RegisterDescription& description = registerDescriptions().at(address >> 1);
238 int i = 0;
239 for (const RegisterItem& ri : description) {
240 QLabel* label = new QLabel(ri.description);
241 box->addWidget(label, i, 0);
242 if (ri.size == 1) {
243 QCheckBox* check = new QCheckBox;
244 check->setEnabled(!ri.readonly);
245 box->addWidget(check, i, 1, Qt::AlignRight);
246 connect(check, SIGNAL(toggled(bool)), m_b[ri.start], SLOT(setChecked(bool)));
247 connect(m_b[ri.start], SIGNAL(toggled(bool)), check, SLOT(setChecked(bool)));
248 } else if (ri.items.empty()) {
249 QSpinBox* sbox = new QSpinBox;
250 sbox->setEnabled(!ri.readonly);
251 sbox->setMaximum((1 << ri.size) - 1);
252 box->addWidget(sbox, i, 1, Qt::AlignRight);
253 for (int b = 0; b < ri.size; ++b) {
254 connect(sbox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this, b, &ri](int v) {
255 m_b[ri.start + b]->setChecked(v & (1 << b));
256 });
257 auto connection = connect(m_b[ri.start + b], &QCheckBox::toggled, [sbox, this, b](bool t) {
258 int v = sbox->value() & ~(1 << b);
259 v |= t << b;
260 bool signalsBlocked = sbox->blockSignals(true);
261 sbox->setValue(v);
262 sbox->blockSignals(signalsBlocked);
263 });
264 connect(sbox, &QObject::destroyed, [connection, this, b, &ri]() {
265 bool signalsBlocked = m_b[ri.start + b]->blockSignals(true);
266 m_b[ri.start + b]->disconnect(connection);
267 m_b[ri.start + b]->blockSignals(signalsBlocked);
268 });
269 }
270 } else {
271 QButtonGroup* group = new QButtonGroup(box);
272 group->setExclusive(true);
273 for (int o = 0; o < 1 << ri.size; ++o) {
274 if (ri.items.at(o).isNull()) {
275 continue;
276 }
277 ++i;
278 QRadioButton* button = new QRadioButton(ri.items.at(o));
279 button->setEnabled(!ri.readonly);
280 box->addWidget(button, i, 0, 1, 2, Qt::AlignLeft);
281 group->addButton(button, o);
282 }
283 for (int b = 0; b < ri.size; ++b) {
284 auto connection = connect(m_b[ri.start + b], &QCheckBox::toggled, [group, this, &ri](bool) {
285 unsigned v = (m_value >> ri.start) & ((1 << ri.size) - 1);
286 for (int i = 0; i < 1 << ri.size; ++i) {
287 QAbstractButton* button = group->button(i);
288 if (!button) {
289 continue;
290 }
291 bool signalsBlocked = button->blockSignals(true);
292 button->setChecked(i == v);
293 button->blockSignals(signalsBlocked);
294 }
295 });
296 connect(group, &QObject::destroyed, [connection, this, b, &ri]() {
297 m_b[ri.start + b]->disconnect(connection);
298 });
299 }
300 connect(group, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), [this, &ri](int v) {
301 for (int i = 0; i < ri.size; ++i) {
302 bool signalsBlocked = m_b[ri.start + i]->blockSignals(true);
303 m_b[ri.start + i]->setChecked(v & (1 << i));
304 m_b[ri.start + i]->blockSignals(signalsBlocked);
305 }
306 });
307 }
308 ++i;
309 }
310 }
311 m_ui.regDescription->setLayout(box);
312 updateRegister();
313}
314
315void IOViewer::selectRegister() {
316 selectRegister(m_ui.regSelect->currentData().toUInt());
317}
318
319void IOViewer::buttonPressed(QAbstractButton* button) {
320 switch (m_ui.buttonBox->standardButton(button)) {
321 case QDialogButtonBox::Reset:
322 updateRegister();
323 break;
324 case QDialogButtonBox::Apply:
325 writeback();
326 break;
327 default:
328 break;
329 }
330}