all repos — mgba @ c4b38790f211b65cb15af26cebe9fc25e3c19914

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