all repos — mgba @ a760c7bb4a85c8abea17ff9db466e96ebe20a8de

mGBA Game Boy Advance Emulator

src/platform/qt/FrameView.cpp (view raw)

  1/* Copyright (c) 2013-2019 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 "FrameView.h"
  7
  8#include <QMouseEvent>
  9#include <QPalette>
 10
 11#include <array>
 12#include <cmath>
 13
 14#include "CoreController.h"
 15#include "GBAApp.h"
 16
 17#include <mgba/core/core.h>
 18#include <mgba/feature/video-logger.h>
 19#ifdef M_CORE_GBA
 20#include <mgba/internal/gba/gba.h>
 21#include <mgba/internal/gba/io.h>
 22#include <mgba/internal/gba/memory.h>
 23#include <mgba/internal/gba/video.h>
 24#endif
 25#ifdef M_CORE_GB
 26#include <mgba/internal/gb/gb.h>
 27#include <mgba/internal/gb/io.h>
 28#include <mgba/internal/gb/memory.h>
 29#endif
 30
 31using namespace QGBA;
 32
 33FrameView::FrameView(std::shared_ptr<CoreController> controller, QWidget* parent)
 34	: AssetView(controller, parent)
 35{
 36	m_ui.setupUi(this);
 37
 38	m_glowTimer.setInterval(33);
 39	connect(&m_glowTimer, &QTimer::timeout, this, [this]() {
 40		++m_glowFrame;
 41		invalidateQueue();
 42	});
 43
 44	m_ui.compositedView->installEventFilter(this);
 45
 46	connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) {
 47		Layer& layer = m_queue[item->data(Qt::UserRole).toInt()];
 48		layer.enabled = item->checkState() == Qt::Checked;
 49		if (layer.enabled) {
 50			m_disabled.remove(layer.id);
 51		} else {
 52			m_disabled.insert(layer.id);
 53		}
 54		invalidateQueue();
 55	});
 56	connect(m_ui.queue, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* item) {
 57		if (item) {
 58			m_active = m_queue[item->data(Qt::UserRole).toInt()].id;
 59		} else {
 60			m_active = {};
 61		}
 62		invalidateQueue();
 63	});
 64	connect(m_ui.magnification, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]() {
 65		invalidateQueue();
 66	});
 67	connect(m_ui.exportButton, &QAbstractButton::pressed, this, &FrameView::exportFrame);
 68	connect(m_ui.reset, &QAbstractButton::pressed, this, &FrameView::reset);
 69
 70	m_backdropPicker = ColorPicker(m_ui.backdrop, QColor(0, 0, 0, 0));
 71	connect(&m_backdropPicker, &ColorPicker::colorChanged, this, [this](const QColor& color) {
 72		m_overrideBackdrop = color;
 73	});
 74	m_controller->addFrameAction(std::bind(&FrameView::frameCallback, this, m_callbackLocker));
 75
 76	{
 77		CoreController::Interrupter interrupter(m_controller);
 78		refreshVl();
 79	}
 80	m_controller->frameAdvance();
 81}
 82
 83FrameView::~FrameView() {
 84	QMutexLocker locker(&m_mutex);
 85	*m_callbackLocker = false;
 86	if (m_vl) {
 87		m_vl->deinit(m_vl);
 88	}
 89}
 90
 91bool FrameView::lookupLayer(const QPointF& coord, Layer*& out) {
 92	for (Layer& layer : m_queue) {
 93		if (!layer.enabled || m_disabled.contains(layer.id)) {
 94			continue;
 95		}
 96		QPointF location = layer.location;
 97		QSizeF layerDims(layer.image.width(), layer.image.height());
 98		QRegion region;
 99		if (layer.repeats) {
100			if (location.x() + layerDims.width() < 0) {
101				location.setX(std::fmod(location.x(), layerDims.width()));
102			}
103			if (location.y() + layerDims.height() < 0) {
104				location.setY(std::fmod(location.y(), layerDims.height()));
105			}
106
107			region += layer.mask.translated(location.x(), location.y());
108			region += layer.mask.translated(location.x() + layerDims.width(), location.y());
109			region += layer.mask.translated(location.x(), location.y() + layerDims.height());
110			region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height());
111		} else {
112			region = layer.mask.translated(location.x(), location.y());
113		}
114
115		if (region.contains(QPoint(coord.x(), coord.y()))) {
116			out = &layer;
117			return true;
118		}
119	}
120	return false;
121}
122
123void FrameView::selectLayer(const QPointF& coord) {
124	Layer* layer;
125	if (!lookupLayer(coord, layer)) {
126		return;
127	}
128	if (layer->id == m_active) {
129		m_active = {};
130	} else {
131		m_active = layer->id;
132	}
133	m_glowFrame = 0;
134}
135
136void FrameView::disableLayer(const QPointF& coord) {
137	Layer* layer;
138	if (!lookupLayer(coord, layer)) {
139		return;
140	}
141	layer->enabled = false;
142	m_disabled.insert(layer->id);
143}
144
145#ifdef M_CORE_GBA
146void FrameView::updateTilesGBA(bool) {
147	if (m_ui.freeze->checkState() == Qt::Checked) {
148		return;
149	}
150	QMutexLocker locker(&m_mutex);
151	m_queue.clear();
152	{
153		CoreController::Interrupter interrupter(m_controller);
154
155		uint16_t* io = static_cast<GBA*>(m_controller->thread()->core->board)->memory.io;
156		QRgb backdrop = M_RGB5_TO_RGB8(static_cast<GBA*>(m_controller->thread()->core->board)->video.palette[0]);
157		GBARegisterDISPCNT gbaDispcnt = io[REG_DISPCNT >> 1];
158		int mode = GBARegisterDISPCNTGetMode(gbaDispcnt);
159
160		std::array<bool, 4> enabled{
161			bool(GBARegisterDISPCNTIsBg0Enable(gbaDispcnt)),
162			bool(GBARegisterDISPCNTIsBg1Enable(gbaDispcnt)),
163			bool(GBARegisterDISPCNTIsBg2Enable(gbaDispcnt)),
164			bool(GBARegisterDISPCNTIsBg3Enable(gbaDispcnt)),
165		};
166
167		if (GBARegisterDISPCNTIsWin0Enable(gbaDispcnt)) {
168			m_queue.append({
169				{ LayerId::WINDOW, 0 },
170				!m_disabled.contains({ LayerId::WINDOW, 0 }),
171				{},
172				{}, {0, 0}, true, false
173			});
174		}
175
176		if (GBARegisterDISPCNTIsWin1Enable(gbaDispcnt)) {
177			m_queue.append({
178				{ LayerId::WINDOW, 1 },
179				!m_disabled.contains({ LayerId::WINDOW, 1 }),
180				{},
181				{}, {0, 0}, true, false
182			});
183		}
184
185		if (GBARegisterDISPCNTIsObjwinEnable(gbaDispcnt)) {
186			m_queue.append({
187				{ LayerId::WINDOW, 2 },
188				!m_disabled.contains({ LayerId::WINDOW, 2 }),
189				{},
190				{}, {0, 0}, true, false
191			});
192
193		}
194
195		for (int priority = 0; priority < 4; ++priority) {
196			for (int sprite = 0; sprite < 128; ++sprite) {
197				ObjInfo info;
198				lookupObj(sprite, &info);
199
200				if (!info.enabled || info.priority != priority) {
201					continue;
202				}
203
204				QPointF offset(info.x, info.y);
205				QImage obj(compositeObj(info));
206				if (info.hflip || info.vflip) {
207					obj = obj.mirrored(info.hflip, info.vflip);
208				}
209				if (!info.xform.isIdentity()) {
210					offset += QPointF(obj.width(), obj.height()) / 2;
211					obj = obj.transformed(info.xform);
212					offset -= QPointF(obj.width() / 2, obj.height() / 2);
213				}
214				m_queue.append({
215					{ LayerId::SPRITE, sprite },
216					!m_disabled.contains({ LayerId::SPRITE, sprite }),
217					QPixmap::fromImage(obj),
218					{}, offset, false, false
219				});
220				if (m_queue.back().image.hasAlpha()) {
221					m_queue.back().mask = QRegion(m_queue.back().image.mask());
222				} else {
223					m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
224				}
225			}
226
227			for (int bg = 0; bg < 4; ++bg) {
228				if (!enabled[bg]) {
229					continue;
230				}
231				if (GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + bg]) != priority) {
232					continue;
233				}
234
235				QPointF offset;
236				if (mode == 0) {
237					offset.setX(-(io[(REG_BG0HOFS >> 1) + (bg << 1)] & 0x1FF));
238					offset.setY(-(io[(REG_BG0VOFS >> 1) + (bg << 1)] & 0x1FF));
239				};
240				m_queue.append({
241					{ LayerId::BACKGROUND, bg },
242					!m_disabled.contains({ LayerId::BACKGROUND, bg }),
243					QPixmap::fromImage(compositeMap(bg, m_mapStatus[bg])),
244					{}, offset, true, false
245				});
246				if (m_queue.back().image.hasAlpha()) {
247					m_queue.back().mask = QRegion(m_queue.back().image.mask());
248				} else {
249					m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
250				}
251			}
252		}
253		QImage backdropImage(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS), QImage::Format_Mono);
254		backdropImage.fill(1);
255		backdropImage.setColorTable({backdrop, backdrop | 0xFF000000 });
256		m_queue.append({
257			{ LayerId::BACKDROP },
258			!m_disabled.contains({ LayerId::BACKDROP }),
259			QPixmap::fromImage(backdropImage),
260			{}, {0, 0}, false, true
261		});
262		updateRendered();
263	}
264	invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS));
265}
266
267void FrameView::injectGBA() {
268	mVideoLogger* logger = m_vl->videoLogger;
269	GBA* gba = static_cast<GBA*>(m_vl->board);
270	gba->video.renderer->highlightBG[0] = false;
271	gba->video.renderer->highlightBG[1] = false;
272	gba->video.renderer->highlightBG[2] = false;
273	gba->video.renderer->highlightBG[3] = false;
274	for (int i = 0; i < 128; ++i) {
275		gba->video.renderer->highlightOBJ[i] = false;
276	}
277	QPalette palette;
278	gba->video.renderer->highlightColor = M_RGB8_TO_NATIVE(palette.color(QPalette::Highlight).rgb());
279	gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 48 + 64;
280	if (!m_overrideBackdrop.isValid()) {
281		QRgb backdrop = M_RGB5_TO_RGB8(gba->video.palette[0]) | 0xFF000000;
282		m_backdropPicker.setColor(backdrop);
283	}
284
285	m_vl->reset(m_vl);
286	for (const Layer& layer : m_queue) {
287		switch (layer.id.type) {
288		case LayerId::SPRITE:
289			if (!layer.enabled) {
290				mVideoLoggerInjectOAM(logger, layer.id.index << 2, 0x200);
291			}
292			if (layer.id == m_active) {
293				gba->video.renderer->highlightOBJ[layer.id.index] = true;
294			}
295			break;
296		case LayerId::BACKGROUND:
297			m_vl->enableVideoLayer(m_vl, layer.id.index, layer.enabled);
298			if (layer.id == m_active) {
299				gba->video.renderer->highlightBG[layer.id.index] = true;
300			}
301			break;
302		case LayerId::WINDOW:
303			m_vl->enableVideoLayer(m_vl, GBA_LAYER_WIN0 + layer.id.index, layer.enabled);
304			break;
305		case LayerId::BACKDROP:
306		case LayerId::FRAME:
307		case LayerId::NONE:
308			break;
309		}
310	}
311	if (m_overrideBackdrop.isValid()) {
312		mVideoLoggerInjectPalette(logger, 0, M_RGB8_TO_RGB5(m_overrideBackdrop.rgb()));
313	}
314}
315#endif
316
317#ifdef M_CORE_GB
318void FrameView::updateTilesGB(bool) {
319	if (m_ui.freeze->checkState() == Qt::Checked) {
320		return;
321	}
322	m_queue.clear();
323	{
324		CoreController::Interrupter interrupter(m_controller);
325		QPointF origin;
326		GB* gb = static_cast<GB*>(m_controller->thread()->core->board);
327		if (gb->video.sgbBorders && (gb->model & GB_MODEL_SGB)) {
328			origin = QPointF(48, 40);
329		}
330		uint8_t* io = gb->memory.io;
331		GBRegisterLCDC lcdc = io[GB_REG_LCDC];
332
333		for (int sprite = 0; sprite < 40; ++sprite) {
334			ObjInfo info;
335			lookupObj(sprite, &info);
336
337			if (!info.enabled) {
338				continue;
339			}
340
341			QPointF offset(info.x, info.y);
342			QImage obj(compositeObj(info));
343			if (info.hflip || info.vflip) {
344				obj = obj.mirrored(info.hflip, info.vflip);
345			}
346			m_queue.append({
347				{ LayerId::SPRITE, sprite },
348				!m_disabled.contains({ LayerId::SPRITE, sprite }),
349				QPixmap::fromImage(obj),
350				{}, offset + origin, false, false
351			});
352			if (m_queue.back().image.hasAlpha()) {
353				m_queue.back().mask = QRegion(m_queue.back().image.mask());
354			} else {
355				m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
356			}
357		}
358
359		if (GBRegisterLCDCIsWindow(lcdc)) {
360			m_queue.append({
361				{ LayerId::WINDOW },
362				!m_disabled.contains({ LayerId::WINDOW }),
363				{},
364				{}, origin, false, false
365			});
366		}
367
368		m_queue.append({
369			{ LayerId::BACKGROUND },
370			!m_disabled.contains({ LayerId::BACKGROUND }),
371			{},
372			{}, origin, false, false
373		});
374
375		updateRendered();
376	}
377	invalidateQueue(m_controller->screenDimensions());
378}
379
380void FrameView::injectGB() {
381	mVideoLogger* logger = m_vl->videoLogger;
382	GB* gb = static_cast<GB*>(m_vl->board);
383	gb->video.renderer->highlightBG = false;
384	gb->video.renderer->highlightWIN = false;
385	for (int i = 0; i < 40; ++i) {
386		gb->video.renderer->highlightOBJ[i] = false;
387	}
388	QPalette palette;
389	gb->video.renderer->highlightColor = M_RGB8_TO_NATIVE(palette.color(QPalette::Highlight).rgb());
390	gb->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 48 + 64;
391
392	m_vl->reset(m_vl);
393	for (const Layer& layer : m_queue) {
394		switch (layer.id.type) {
395		case LayerId::SPRITE:
396			if (!layer.enabled) {
397				mVideoLoggerInjectOAM(logger, layer.id.index << 2, 0);
398			}
399			if (layer.id == m_active) {
400				gb->video.renderer->highlightOBJ[layer.id.index] = true;
401			}
402			break;
403		case LayerId::BACKGROUND:
404			m_vl->enableVideoLayer(m_vl, GB_LAYER_BACKGROUND, layer.enabled);
405			if (layer.id == m_active) {
406				gb->video.renderer->highlightBG = true;
407			}
408			break;
409		case LayerId::WINDOW:
410			m_vl->enableVideoLayer(m_vl, GB_LAYER_WINDOW, layer.enabled);
411			if (layer.id == m_active) {
412				gb->video.renderer->highlightWIN = true;
413			}
414			break;
415		case LayerId::FRAME: // TODO for SGB
416		case LayerId::BACKDROP:
417		case LayerId::NONE:
418			break;
419		}
420	}
421}
422#endif
423
424void FrameView::invalidateQueue(const QSize& dims) {
425	if (dims.isValid()) {
426		m_dims = dims;
427	}
428	bool blockSignals = m_ui.queue->blockSignals(true);
429	QMutexLocker locker(&m_mutex);
430	if (m_vl) {
431		mVideoLogger* logger = m_vl->videoLogger;
432		mVideoLoggerInjectionPoint(logger, LOGGER_INJECTION_FIRST_SCANLINE);
433		switch (m_controller->platform()) {
434#ifdef M_CORE_GBA
435		case mPLATFORM_GBA:
436			injectGBA();
437			break;
438#endif
439#ifdef M_CORE_GB
440		case mPLATFORM_GB:
441			injectGB();
442			break;
443#endif
444		case mPLATFORM_NONE:
445			break;
446		}
447		if (m_ui.disableScanline->checkState() == Qt::Checked) {
448			mVideoLoggerIgnoreAfterInjection(logger, (1 << DIRTY_PALETTE) | (1 << DIRTY_OAM) | (1 << DIRTY_REGISTER));
449		} else {
450			mVideoLoggerIgnoreAfterInjection(logger, 0);
451		}
452		m_vl->runFrame(m_vl);
453	}
454
455	for (int i = 0; i < m_queue.count(); ++i) {
456		const Layer& layer = m_queue[i];
457		QListWidgetItem* item;
458		if (i >= m_ui.queue->count()) {
459			item = new QListWidgetItem;
460			m_ui.queue->addItem(item);
461		} else {
462			item = m_ui.queue->item(i);
463		}
464		item->setText(layer.id.readable());
465		item->setFlags(Qt::ItemIsSelectable | (layer.fixed ? Qt::NoItemFlags : Qt::ItemIsUserCheckable) | Qt::ItemIsEnabled);
466		item->setCheckState(layer.enabled ? Qt::Checked : Qt::Unchecked);
467		item->setData(Qt::UserRole, i);
468		item->setSelected(layer.id == m_active);
469	}
470
471	while (m_ui.queue->count() > m_queue.count()) {
472		delete m_ui.queue->takeItem(m_queue.count());
473	}
474	m_ui.queue->blockSignals(blockSignals);
475
476	QPixmap composited;
477	if (m_framebuffer.isNull()) {
478		updateRendered();
479		composited = m_rendered;
480	} else {
481		QImage framebuffer(m_framebuffer);
482		m_ui.exportButton->setEnabled(true);
483		if (framebuffer.size() != m_dims) {
484			framebuffer = framebuffer.copy({QPoint(), m_dims});
485		}
486		composited.convertFromImage(framebuffer);
487	}
488	m_composited = composited.scaled(m_dims * m_ui.magnification->value());
489	m_ui.compositedView->setPixmap(m_composited);
490}
491
492void FrameView::updateRendered() {
493	if (m_ui.freeze->checkState() == Qt::Checked) {
494		return;
495	}
496	m_rendered.convertFromImage(m_controller->getPixels());
497}
498
499bool FrameView::eventFilter(QObject*, QEvent* event) {
500	QPointF pos;
501	switch (event->type()) {
502	case QEvent::MouseButtonPress:
503		pos = static_cast<QMouseEvent*>(event)->localPos();
504		pos /= m_ui.magnification->value();
505		selectLayer(pos);
506		return true;
507	case QEvent::MouseButtonDblClick:
508		pos = static_cast<QMouseEvent*>(event)->localPos();
509		pos /= m_ui.magnification->value();
510		disableLayer(pos);
511		return true;
512	default:
513		break;
514	}
515	return false;
516}
517
518void FrameView::refreshVl() {
519	QMutexLocker locker(&m_mutex);
520	m_currentFrame = m_nextFrame;
521	m_nextFrame = VFileDevice::openMemory();
522	if (m_currentFrame) {
523		m_controller->endVideoLog(false);
524		QMetaObject::invokeMethod(this, "newVl");
525	}
526	m_controller->endVideoLog();
527	m_controller->startVideoLog(m_nextFrame, false);
528}
529
530void FrameView::newVl() {
531	if (!m_glowTimer.isActive()) {
532		m_glowTimer.start();
533	}
534	QMutexLocker locker(&m_mutex);
535	if (!m_currentFrame) {
536		return;
537	}
538	if (m_vl) {
539		m_vl->deinit(m_vl);
540	}
541	m_vl = mCoreFindVF(m_currentFrame);
542	m_vl->init(m_vl);
543	m_vl->loadROM(m_vl, m_currentFrame);
544	m_currentFrame = nullptr;
545	mCoreInitConfig(m_vl, nullptr);
546#ifdef M_CORE_GB
547	if (m_controller->platform() == mPLATFORM_GB) {
548		mCoreConfigSetIntValue(&m_vl->config, "sgb.borders", static_cast<GB*>(m_controller->thread()->core->board)->video.sgbBorders);
549		m_vl->reloadConfigOption(m_vl, "sgb.borders", nullptr);
550	}
551#endif
552	unsigned width, height;
553	m_vl->desiredVideoDimensions(m_vl, &width, &height);
554	m_framebuffer = QImage(width, height, QImage::Format_RGBX8888);
555	m_vl->setVideoBuffer(m_vl, reinterpret_cast<color_t*>(m_framebuffer.bits()), width);
556	m_vl->reset(m_vl);
557}
558
559void FrameView::frameCallback(FrameView* viewer, std::shared_ptr<bool> lock) {
560	if (!*lock) {
561		return;
562	}
563	CoreController::Interrupter interrupter(viewer->m_controller);
564	viewer->refreshVl();
565	viewer->m_controller->addFrameAction(std::bind(&FrameView::frameCallback, viewer, lock));
566}
567
568void FrameView::exportFrame() {
569	QString filename = GBAApp::app()->getSaveFileName(this, tr("Export frame"),
570	                                                  tr("Portable Network Graphics (*.png)"));
571	CoreController::Interrupter interrupter(m_controller);
572	m_framebuffer.save(filename, "PNG");
573}
574
575void FrameView::reset() {
576	m_disabled.clear();
577	for (Layer& layer : m_queue) {
578		layer.enabled = true;
579	}
580	m_overrideBackdrop = QColor();
581	invalidateQueue();
582}
583
584QString FrameView::LayerId::readable() const {
585	QString typeStr;
586	switch (type) {
587	case NONE:
588		return tr("None");
589	case BACKGROUND:
590		typeStr = tr("Background");
591		break;
592	case WINDOW:
593		typeStr = tr("Window");
594#ifdef M_CORE_GBA
595		if (index == 2) {
596			return tr("Objwin");
597		}
598#endif
599		break;
600	case SPRITE:
601		typeStr = tr("Sprite");
602		break;
603	case BACKDROP:
604		typeStr = tr("Backdrop");
605		break;
606	case FRAME:
607		typeStr = tr("Frame");
608		break;
609	}
610	if (index < 0) {
611		return typeStr;
612	}
613	return tr("%1 %2").arg(typeStr).arg(index);
614}