all repos — mgba @ 33098926577f52a74730de1708a427e275a9bc88

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