all repos — mgba @ 2498f85cda6a8d3e2b4904a920b84d2cc9ffb02a

mGBA Game Boy Advance Emulator

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.data())) {
262		return;
263	}
264	unsigned vendor[4] = { regs[1], regs[3], regs[2], 0 };
265	std::array<unsigned, 13> cpu{};
266	cpuid(0x80000002, &cpu[0]);
267	cpuid(0x80000003, &cpu[4]);
268	cpuid(0x80000004, &cpu[8]);
269
270	auto testBit = [](unsigned bit, unsigned reg) {
271		return yesNo[bool(reg & (1 << bit))];
272	};
273	QStringList features;
274	report << QString("CPU: %1").arg(QLatin1String(reinterpret_cast<char*>(cpu.data())));
275	report << QString("CPU manufacturer: %1").arg(QLatin1String(reinterpret_cast<char*>(vendor)));
276	cpuid(1, regs.data());
277	unsigned family = ((regs[0] >> 8) & 0xF) | ((regs[0] >> 16) & 0xFF0);
278	unsigned model = ((regs[0] >> 4) & 0xF) | ((regs[0] >> 12) & 0xF0);
279	report << QString("CPU family ID: %1h").arg(family, 2, 16, QChar('0'));
280	report << QString("CPU model ID: %1h").arg(model, 2, 16, QChar('0'));
281	features << QString("Supports SSE: %1").arg(testBit(25, regs[3]));
282	features << QString("Supports SSE2: %1").arg(testBit(26, regs[3]));
283	features << QString("Supports SSE3: %1").arg(testBit(0, regs[2]));
284	features << QString("Supports SSSE3: %1").arg(testBit(9, regs[2]));
285	features << QString("Supports SSE4.1: %1").arg(testBit(19, regs[2]));
286	features << QString("Supports SSE4.2: %1").arg(testBit(20, regs[2]));
287	features << QString("Supports MOVBE: %1").arg(testBit(22, regs[2]));
288	features << QString("Supports POPCNT: %1").arg(testBit(23, regs[2]));
289	features << QString("Supports RDRAND: %1").arg(testBit(30, regs[2]));
290	features << QString("Supports AVX: %1").arg(testBit(28, regs[2]));
291	features << QString("Supports CMPXCHG8: %1").arg(testBit(8, regs[3]));
292	features << QString("Supports CMPXCHG16: %1").arg(testBit(13, regs[2]));
293	cpuid(7, 0, regs.data());
294	features << QString("Supports AVX2: %1").arg(testBit(5, regs[1]));
295	features << QString("Supports BMI1: %1").arg(testBit(3, regs[1]));
296	features << QString("Supports BMI2: %1").arg(testBit(8, regs[1]));
297	cpuid(0x80000001, regs.data());
298	features << QString("Supports ABM: %1").arg(testBit(5, regs[2]));
299	features << QString("Supports SSE4a: %1").arg(testBit(6, regs[2]));
300	features.sort();
301	report << features;
302#endif
303}
304
305void ReportView::addGLInfo(QStringList& report) {
306#ifdef DISPLAY_GL_INFO
307	QSurfaceFormat format;
308
309	report << QString("OpenGL type: %1").arg(QLatin1String(QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL ? "OpenGL" : "OpenGL|ES"));
310
311	format.setVersion(1, 4);
312	report << QString("OpenGL supports legacy (1.x) contexts: %1").arg(yesNo[DisplayGL::supportsFormat(format)]);
313
314	if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) {
315		format.setVersion(2, 0);
316	} else {
317		format.setVersion(3, 2);
318	}
319	format.setProfile(QSurfaceFormat::CoreProfile);
320	report << QString("OpenGL supports core contexts: %1").arg(yesNo[DisplayGL::supportsFormat(format)]);
321
322	QOpenGLContext context;
323	if (context.create()) {
324		QOffscreenSurface surface;
325		surface.create();
326		context.makeCurrent(&surface);
327		report << QString("OpenGL renderer: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_RENDERER))));
328		report << QString("OpenGL vendor: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_VENDOR))));
329		report << QString("OpenGL version string: %1").arg(QLatin1String(reinterpret_cast<const char*>(context.functions()->glGetString(GL_VERSION))));
330	}
331#else
332	report << QString("OpenGL support disabled at compilation time");
333#endif
334}
335
336void ReportView::addROMInfo(QStringList& report, CoreController* controller) {
337	report << QString("Currently paused: %1").arg(yesNo[controller->isPaused()]);
338
339	mCore* core = controller->thread()->core;
340	char title[17] = {};
341	core->getGameTitle(core, title);
342	report << QString("Internal title: %1").arg(QLatin1String(title));
343
344	title[8] = '\0';
345	core->getGameCode(core, title);
346	if (title[0]) {
347		report << QString("Game code: %1").arg(QLatin1String(title));
348	} else {
349		report << QString("Invalid game code");
350	}
351
352	uint32_t crc32 = 0;
353	core->checksum(core, &crc32, mCHECKSUM_CRC32);
354	report << QString("CRC32: %1").arg(crc32, 8, 16, QChar('0'));
355
356#ifdef USE_SQLITE3
357	const NoIntroDB* db = GBAApp::app()->gameDB();
358	if (db && crc32) {
359		NoIntroGame game{};
360		if (NoIntroDBLookupGameByCRC(db, crc32, &game)) {
361			report << QString("No-Intro name: %1").arg(game.name);
362		} else {
363			report << QString("Not present in No-Intro database").arg(game.name);
364		}
365	}
366#endif
367}
368
369void ReportView::addScreenInfo(QStringList& report, const QScreen* screen) {
370	QRect geometry = screen->geometry();
371
372	report << QString("Size: %1x%2").arg(geometry.width()).arg(geometry.height());
373	report << QString("Location: %1, %2").arg(geometry.x()).arg(geometry.y());
374	report << QString("Refresh rate: %1 Hz").arg(screen->refreshRate());
375#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
376	report << QString("Pixel ratio: %1").arg(screen->devicePixelRatio());
377#endif
378	report << QString("Logical DPI: %1x%2").arg(screen->logicalDotsPerInchX()).arg(screen->logicalDotsPerInchY());
379	report << QString("Physical DPI: %1x%2").arg(screen->physicalDotsPerInchX()).arg(screen->physicalDotsPerInchY());
380}
381
382void ReportView::addReport(const QString& filename, const QString& report) {
383	m_reports[filename] = report;
384	m_displayOrder.append(filename);
385}
386
387void ReportView::addBinary(const QString& filename, const QByteArray& binary) {
388	m_binaries[filename] = binary;
389	m_displayOrder.append(filename);
390}
391
392QString ReportView::redact(const QString& text) {
393	static QRegularExpression home(R"((?:\b|^)[A-Z]:[\\/](?:Users|Documents and Settings)[\\/][^\\/]+|(?:/usr)?/home/[^/]+)",
394	                               QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption);
395	QString redacted = text;
396	redacted.replace(home, QString("[Home directory]"));
397	return redacted;
398}
399
400bool ReportView::eventFilter(QObject*, QEvent* event) {
401	if (event->type() != QEvent::FocusOut) {
402		QListWidgetItem* currentReport = m_ui.fileList->currentItem();
403		if (currentReport && !currentReport->text().isNull()) {
404			m_reports[currentReport->text()] = m_ui.fileView->toPlainText();
405		}
406	}
407	return false;
408}
409
410#ifdef USE_CPUID
411bool ReportView::cpuid(unsigned id, unsigned* regs) {
412	return cpuid(id, 0, regs);
413}
414
415bool ReportView::cpuid(unsigned id, unsigned sub, unsigned* regs) {
416	if (s_cpuidMax == 0xFFFFFFFF) {
417#ifdef _MSC_VER
418		__cpuid(reinterpret_cast<int*>(regs), 0);
419		s_cpuidMax = regs[0];
420		__cpuid(reinterpret_cast<int*>(regs), 0x80000000);
421		s_cpuidExtMax = regs[0];
422#else
423		s_cpuidMax = __get_cpuid_max(0, nullptr);
424		s_cpuidExtMax = __get_cpuid_max(0x80000000, nullptr);
425#endif
426	}
427	regs[0] = 0;
428	regs[1] = 0;
429	regs[2] = 0;
430	regs[3] = 0;
431	if (!(id & 0x80000000) && id > s_cpuidMax) {
432		return false;
433	}
434	if ((id & 0x80000000) && id > s_cpuidExtMax) {
435		return false;
436	}
437
438#ifdef _MSC_VER
439	__cpuidex(reinterpret_cast<int*>(regs), id, sub);
440#else
441	__cpuid_count(id, sub, regs[0], regs[1], regs[2], regs[3]);
442#endif
443	return true;
444}
445#endif