src/platform/qt/ReportView.cpp (view raw)
1/* Copyright (c) 2013-2020 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 "ReportView.h"
7
8#include <QBuffer>
9#include <QDesktopServices>
10#include <QOffscreenSurface>
11#include <QScreen>
12#include <QSysInfo>
13#include <QWindow>
14
15#include <mgba/core/version.h>
16#include <mgba-util/vfs.h>
17
18#include "CoreController.h"
19#include "GBAApp.h"
20#include "Window.h"
21
22#include "ui_ReportView.h"
23
24#if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))
25#define USE_CPUID
26#include <cpuid.h>
27#endif
28#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
29#define USE_CPUID
30#endif
31
32#if defined(BUILD_GL) || defined(BUILD_GLES2) || defined(BUILD_GLES3) || defined(USE_EPOXY)
33#define DISPLAY_GL_INFO
34
35#include "DisplayGL.h"
36
37#include <QOpenGLFunctions>
38#endif
39
40#ifdef USE_SQLITE3
41#include "feature/sqlite3/no-intro.h"
42#endif
43
44using namespace QGBA;
45
46static const QLatin1String yesNo[2] = {
47 QLatin1String("No"),
48 QLatin1String("Yes")
49};
50
51#ifdef USE_CPUID
52unsigned ReportView::s_cpuidMax = 0xFFFFFFFF;
53unsigned ReportView::s_cpuidExtMax = 0xFFFFFFFF;
54#endif
55
56ReportView::ReportView(QWidget* parent)
57 : QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint)
58{
59 m_ui.setupUi(this);
60
61 QString description = m_ui.description->text();
62 description.replace("{projectName}", QLatin1String(projectName));
63 m_ui.description->setText(description);
64
65 connect(m_ui.fileList, &QListWidget::currentTextChanged, this, &ReportView::setShownReport);
66}
67
68void ReportView::generateReport() {
69 m_displayOrder.clear();
70 m_reports.clear();
71
72 QDir configDir(ConfigController::configDir());
73
74 QStringList swReport;
75 swReport << QString("Name: %1").arg(QLatin1String(projectName));
76 swReport << QString("Executable location: %1").arg(redact(QCoreApplication::applicationFilePath()));
77 swReport << QString("Portable: %1").arg(yesNo[ConfigController::isPortable()]);
78 swReport << QString("Configuration directory: %1").arg(redact(configDir.path()));
79 swReport << QString("Version: %1").arg(QLatin1String(projectVersion));
80 swReport << QString("Git branch: %1").arg(QLatin1String(gitBranch));
81 swReport << QString("Git commit: %1").arg(QLatin1String(gitCommit));
82 swReport << QString("Git revision: %1").arg(gitRevision);
83 swReport << QString("OS: %1").arg(QSysInfo::prettyProductName());
84 swReport << QString("Build architecture: %1").arg(QSysInfo::buildCpuArchitecture());
85 swReport << QString("Run architecture: %1").arg(QSysInfo::currentCpuArchitecture());
86 swReport << QString("Qt version: %1").arg(QLatin1String(qVersion()));
87 addReport(QString("System info"), swReport.join('\n'));
88
89 QStringList hwReport;
90 addCpuInfo(hwReport);
91 addGLInfo(hwReport);
92 addReport(QString("Hardware info"), hwReport.join('\n'));
93
94 QList<QScreen*> screens = QGuiApplication::screens();
95 std::sort(screens.begin(), screens.end(), [](const QScreen* a, const QScreen* b) {
96 if (a->geometry().y() < b->geometry().y()) {
97 return true;
98 }
99 if (a->geometry().x() < b->geometry().x()) {
100 return true;
101 }
102 return false;
103 });
104
105 int screenId = 0;
106 for (const QScreen* screen : screens) {
107 ++screenId;
108 QStringList screenReport;
109 addScreenInfo(screenReport, screen);
110 addReport(QString("Screen %1").arg(screenId), screenReport.join('\n'));
111 }
112
113 QList<QPair<QString, QByteArray>> deferredBinaries;
114 QList<ConfigController*> configs;
115 int winId = 0;
116 for (auto window : GBAApp::app()->windows()) {
117 ++winId;
118 QStringList windowReport;
119 auto controller = window->controller();
120 ConfigController* config = window->config();
121 if (configs.indexOf(config) < 0) {
122 configs.append(config);
123 }
124
125 windowReport << QString("Window size: %1x%2").arg(window->width()).arg(window->height());
126 windowReport << QString("Window location: %1, %2").arg(window->x()).arg(window->y());
127#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
128 QScreen* screen = window->screen();
129#else
130 QScreen* screen = NULL;
131 if (window->windowHandle()) {
132 screen = window->windowHandle()->screen();
133 }
134#endif
135 if (screen && screens.contains(screen)) {
136 windowReport << QString("Screen: %1").arg(screens.contains(screen) + 1);
137 } else {
138 windowReport << QString("Screen: Unknown");
139 }
140 if (controller) {
141 windowReport << QString("ROM open: Yes");
142
143 {
144 CoreController::Interrupter interrupter(controller);
145 addROMInfo(windowReport, controller.get());
146
147 if (m_ui.includeSave->isChecked() && !m_ui.includeState->isChecked()) {
148 // Only do the save separately if savestates aren't enabled, to guarantee consistency
149 mCore* core = controller->thread()->core;
150 void* sram = NULL;
151 size_t size = core->savedataClone(core, &sram);
152 if (sram) {
153 QByteArray save(static_cast<const char*>(sram), size);
154 free(sram);
155 deferredBinaries.append(qMakePair(QString("Save %1").arg(winId), save));
156 }
157 }
158 }
159 if (m_ui.includeState->isChecked()) {
160 QBuffer state;
161 int flags = SAVESTATE_SCREENSHOT | SAVESTATE_CHEATS | SAVESTATE_RTC | SAVESTATE_METADATA;
162 if (m_ui.includeSave->isChecked()) {
163 flags |= SAVESTATE_SAVEDATA;
164 }
165 controller->saveState(&state, flags);
166 deferredBinaries.append(qMakePair(QString("State %1").arg(winId), state.buffer()));
167 if (m_ui.includeSave->isChecked()) {
168 VFile* vf = VFileDevice::wrap(&state, QIODevice::ReadOnly);
169 mStateExtdata extdata;
170 mStateExtdataItem savedata;
171 mStateExtdataInit(&extdata);
172 if (mCoreExtractExtdata(controller->thread()->core, vf, &extdata) && mStateExtdataGet(&extdata, EXTDATA_SAVEDATA, &savedata)) {
173 QByteArray save(static_cast<const char*>(savedata.data), savedata.size);
174 deferredBinaries.append(qMakePair(QString("Save %1").arg(winId), save));
175 }
176 mStateExtdataDeinit(&extdata);
177 }
178 }
179 } else {
180 windowReport << QString("ROM open: No");
181 }
182 windowReport << QString("Configuration: %1").arg(configs.indexOf(config) + 1);
183 addReport(QString("Window %1").arg(winId), windowReport.join('\n'));
184 }
185 for (ConfigController* config : configs) {
186 VFile* vf = VFileDevice::openMemory();
187 mCoreConfigSaveVFile(config->config(), vf);
188 void* contents = vf->map(vf, vf->size(vf), MAP_READ);
189 if (contents) {
190 QString report(QString::fromUtf8(static_cast<const char*>(contents), vf->size(vf)));
191 addReport(QString("Configuration %1").arg(configs.indexOf(config) + 1), redact(report));
192 vf->unmap(vf, contents, vf->size(vf));
193 }
194 vf->close(vf);
195 }
196
197 QFile qtIni(configDir.filePath("qt.ini"));
198 if (qtIni.open(QIODevice::ReadOnly | QIODevice::Text)) {
199 addReport(QString("Qt Configuration"), redact(QString::fromUtf8(qtIni.readAll())));
200 qtIni.close();
201 }
202
203 std::sort(deferredBinaries.begin(), deferredBinaries.end());
204 for (auto& pair : deferredBinaries) {
205 addBinary(pair.first, pair.second);
206 }
207
208 rebuildModel();
209}
210
211void ReportView::save() {
212 QString filename = GBAApp::app()->getSaveFileName(this, tr("Bug report archive"), tr("ZIP archive (*.zip)"));
213 if (filename.isNull()) {
214 return;
215 }
216 VDir* zip = VDirOpenZip(filename.toLocal8Bit().constData(), O_WRONLY | O_CREAT | O_TRUNC);
217 if (!zip) {
218 return;
219 }
220 for (const auto& filename : m_displayOrder) {
221 VFileDevice vf(zip->openFile(zip, filename.toLocal8Bit().constData(), O_WRONLY));
222 if (m_reports.contains(filename)) {
223 vf.setTextModeEnabled(true);
224 vf.write(m_reports[filename].toUtf8());
225 } else if (m_binaries.contains(filename)) {
226 vf.write(m_binaries[filename]);
227 }
228 vf.close();
229 }
230 zip->close(zip);
231}
232
233void ReportView::setShownReport(const QString& filename) {
234 m_ui.fileView->setPlainText(m_reports[filename]);
235}
236
237void ReportView::rebuildModel() {
238 m_ui.fileList->clear();
239 for (const auto& filename : m_displayOrder) {
240 QListWidgetItem* item = new QListWidgetItem(filename);
241 if (m_binaries.contains(filename)) {
242 item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
243 }
244 m_ui.fileList->addItem(item);
245 }
246 m_ui.save->setEnabled(true);
247 m_ui.fileList->setEnabled(true);
248 m_ui.fileView->setEnabled(true);
249 m_ui.openList->setEnabled(true);
250 m_ui.fileList->setCurrentRow(0);
251 m_ui.fileView->installEventFilter(this);
252}
253
254void ReportView::openBugReportPage() {
255 QDesktopServices::openUrl(QUrl("https://mgba.io/i/"));
256}
257
258void ReportView::addCpuInfo(QStringList& report) {
259#ifdef USE_CPUID
260 std::array<unsigned, 4> regs;
261 if (!cpuid(0, regs)) {
262 return;
263 }
264 unsigned vendor[4] = { regs[1], regs[3], regs[2], 0 };
265 auto testBit = [](unsigned bit, unsigned reg) {
266 return yesNo[bool(reg & (1 << bit))];
267 };
268 QStringList features;
269 report << QString("CPU manufacturer: %1").arg(QLatin1String(reinterpret_cast<char*>(vendor)));
270 cpuid(1, regs);
271 unsigned family = ((regs[0] >> 8) & 0xF) | ((regs[0] >> 16) & 0xFF0);
272 unsigned model = ((regs[0] >> 4) & 0xF) | ((regs[0] >> 12) & 0xF0);
273 report << QString("CPU family: %1h").arg(family, 2, 16, QChar('0'));
274 report << QString("CPU model: %1h").arg(model, 2, 16, QChar('0'));
275 features << QString("Supports SSE: %1").arg(testBit(25, regs[3]));
276 features << QString("Supports SSE2: %1").arg(testBit(26, regs[3]));
277 features << QString("Supports SSE3: %1").arg(testBit(0, regs[2]));
278 features << QString("Supports SSSE3: %1").arg(testBit(9, regs[2]));
279 features << QString("Supports SSE4.1: %1").arg(testBit(19, regs[2]));
280 features << QString("Supports SSE4.2: %1").arg(testBit(20, regs[2]));
281 features << QString("Supports MOVBE: %1").arg(testBit(22, regs[2]));
282 features << QString("Supports POPCNT: %1").arg(testBit(23, regs[2]));
283 features << QString("Supports RDRAND: %1").arg(testBit(30, regs[2]));
284 features << QString("Supports AVX: %1").arg(testBit(28, regs[2]));
285 cpuid(7, 0, regs);
286 features << QString("Supports AVX2: %1").arg(testBit(5, regs[1]));
287 features << QString("Supports BMI1: %1").arg(testBit(3, regs[1]));
288 features << QString("Supports BMI2: %1").arg(testBit(8, regs[1]));
289 cpuid(0x80000001, regs);
290 features << QString("Supports ABM: %1").arg(testBit(5, regs[2]));
291 features << QString("Supports SSE4a: %1").arg(testBit(6, regs[2]));
292 features.sort();
293 report << features;
294#endif
295}
296
297void ReportView::addGLInfo(QStringList& report) {
298#ifdef DISPLAY_GL_INFO
299 QSurfaceFormat format;
300
301 report << QString("OpenGL type: %1").arg(QLatin1String(QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL ? "OpenGL" : "OpenGL|ES"));
302
303 format.setVersion(1, 4);
304 report << QString("OpenGL supports legacy (1.x) contexts: %1").arg(yesNo[DisplayGL::supportsFormat(format)]);
305
306 if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) {
307 format.setVersion(2, 0);
308 } else {
309 format.setVersion(3, 2);
310 }
311 format.setProfile(QSurfaceFormat::CoreProfile);
312 report << QString("OpenGL supports core contexts: %1").arg(yesNo[DisplayGL::supportsFormat(format)]);
313
314 QOpenGLContext context;
315 if (context.create()) {
316 QOffscreenSurface surface;
317 surface.create();
318 context.makeCurrent(&surface);
319 report << QString("OpenGL renderer: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_RENDERER))));
320 report << QString("OpenGL vendor: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_VENDOR))));
321 report << QString("OpenGL version string: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_VERSION))));
322 }
323#else
324 report << QString("OpenGL support disabled at compilation time");
325#endif
326}
327
328void ReportView::addROMInfo(QStringList& report, CoreController* controller) {
329 report << QString("Currently paused: %1").arg(yesNo[controller->isPaused()]);
330
331 mCore* core = controller->thread()->core;
332 char title[17] = {};
333 core->getGameTitle(core, title);
334 report << QString("Internal title: %1").arg(QLatin1String(title));
335
336 title[8] = '\0';
337 core->getGameCode(core, title);
338 if (title[0]) {
339 report << QString("Game code: %1").arg(QLatin1String(title));
340 } else {
341 report << QString("Invalid game code");
342 }
343
344 uint32_t crc32 = 0;
345 core->checksum(core, &crc32, CHECKSUM_CRC32);
346 report << QString("CRC32: %1").arg(crc32, 8, 16, QChar('0'));
347
348#ifdef USE_SQLITE3
349 const NoIntroDB* db = GBAApp::app()->gameDB();
350 if (db && crc32) {
351 NoIntroGame game{};
352 if (NoIntroDBLookupGameByCRC(db, crc32, &game)) {
353 report << QString("No-Intro name: %1").arg(game.name);
354 } else {
355 report << QString("Not present in No-Intro database").arg(game.name);
356 }
357 }
358#endif
359}
360
361void ReportView::addScreenInfo(QStringList& report, const QScreen* screen) {
362 QRect geometry = screen->geometry();
363
364 report << QString("Size: %1x%2").arg(geometry.width()).arg(geometry.height());
365 report << QString("Location: %1, %2").arg(geometry.x()).arg(geometry.y());
366 report << QString("Refresh rate: %1 Hz").arg(screen->refreshRate());
367#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
368 report << QString("Pixel ratio: %1").arg(screen->devicePixelRatio());
369#endif
370 report << QString("Logical DPI: %1x%2").arg(screen->logicalDotsPerInchX()).arg(screen->logicalDotsPerInchY());
371 report << QString("Physical DPI: %1x%2").arg(screen->physicalDotsPerInchX()).arg(screen->physicalDotsPerInchY());
372}
373
374void ReportView::addReport(const QString& filename, const QString& report) {
375 m_reports[filename] = report;
376 m_displayOrder.append(filename);
377}
378
379void ReportView::addBinary(const QString& filename, const QByteArray& binary) {
380 m_binaries[filename] = binary;
381 m_displayOrder.append(filename);
382}
383
384QString ReportView::redact(const QString& text) {
385 static QRegularExpression home(R"((?:\b|^)[A-Z]:[\\/](?:Users|Documents and Settings)[\\/][^\\/]+|(?:/usr)?/home/[^/]+)",
386 QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption);
387 QString redacted = text;
388 redacted.replace(home, QString("[Home directory]"));
389 return redacted;
390}
391
392bool ReportView::eventFilter(QObject*, QEvent* event) {
393 if (event->type() != QEvent::FocusOut) {
394 QListWidgetItem* currentReport = m_ui.fileList->currentItem();
395 if (currentReport && !currentReport->text().isNull()) {
396 m_reports[currentReport->text()] = m_ui.fileView->toPlainText();
397 }
398 }
399 return false;
400}
401
402#ifdef USE_CPUID
403bool ReportView::cpuid(unsigned id, std::array<unsigned, 4>& regs) {
404 return cpuid(id, 0, regs);
405}
406
407bool ReportView::cpuid(unsigned id, unsigned sub, std::array<unsigned, 4>& regs) {
408 if (s_cpuidMax == 0xFFFFFFFF) {
409#ifdef _MSC_VER
410 __cpuid(reinterpret_cast<int*>(regs.data()), 0);
411 s_cpuidMax = regs[0];
412 __cpuid(reinterpret_cast<int*>(regs.data()), 0x80000000);
413 s_cpuidExtMax = regs[0];
414#else
415 s_cpuidMax = __get_cpuid_max(0, nullptr);
416 s_cpuidExtMax = __get_cpuid_max(0x80000000, nullptr);
417#endif
418 }
419 regs[0] = 0;
420 regs[1] = 0;
421 regs[2] = 0;
422 regs[3] = 0;
423 if (!(id & 0x80000000) && id > s_cpuidMax) {
424 return false;
425 }
426 if ((id & 0x80000000) && id > s_cpuidExtMax) {
427 return false;
428 }
429
430#ifdef _MSC_VER
431 __cpuidex(reinterpret_cast<int*>(regs.data()), id, sub);
432#else
433 __cpuid_count(id, sub, regs[0], regs[1], regs[2], regs[3]);
434#endif
435 return true;
436}
437#endif