all repos — mgba @ 769678f18ae9890427d664b4cf6df6620dd1a0f2

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/memory.h>
 28#endif
 29
 30using namespace QGBA;
 31
 32FrameView::FrameView(std::shared_ptr<CoreController> controller, QWidget* parent)
 33	: AssetView(controller, parent)
 34{
 35	m_ui.setupUi(this);
 36
 37	m_glowTimer.setInterval(33);
 38	connect(&m_glowTimer, &QTimer::timeout, this, [this]() {
 39		++m_glowFrame;
 40		invalidateQueue();
 41	});
 42
 43	m_ui.compositedView->installEventFilter(this);
 44
 45	connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) {
 46		Layer& layer = m_queue[item->data(Qt::UserRole).toInt()];
 47		layer.enabled = item->checkState() == Qt::Checked;
 48		if (layer.enabled) {
 49			m_disabled.remove(layer.id);
 50		} else {
 51			m_disabled.insert(layer.id);
 52		}
 53		invalidateQueue();
 54	});
 55	connect(m_ui.queue, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* item) {
 56		if (item) {
 57			m_active = m_queue[item->data(Qt::UserRole).toInt()].id;
 58		} else {
 59			m_active = {};
 60		}
 61		invalidateQueue();
 62	});
 63	connect(m_ui.magnification, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]() {
 64		invalidateQueue();
 65	});
 66	connect(m_ui.exportButton, &QAbstractButton::pressed, this, &FrameView::exportFrame);
 67	connect(m_ui.reset, &QAbstractButton::pressed, this, &FrameView::reset);
 68
 69	m_backdropPicker = ColorPicker(m_ui.backdrop, QColor(0, 0, 0, 0));
 70	connect(&m_backdropPicker, &ColorPicker::colorChanged, this, [this](const QColor& color) {
 71		m_overrideBackdrop = color;
 72	});
 73	m_controller->addFrameAction(std::bind(&FrameView::frameCallback, this, m_callbackLocker));
 74
 75	{
 76		CoreController::Interrupter interrupter(m_controller);
 77		refreshVl();
 78	}
 79	m_controller->frameAdvance();
 80}
 81
 82FrameView::~FrameView() {
 83	QMutexLocker locker(&m_mutex);
 84	*m_callbackLocker = false;
 85	if (m_vl) {
 86		m_vl->deinit(m_vl);
 87	}
 88}
 89
 90bool FrameView::lookupLayer(const QPointF& coord, Layer*& out) {
 91	for (Layer& layer : m_queue) {
 92		if (!layer.enabled || m_disabled.contains(layer.id)) {
 93			continue;
 94		}
 95		QPointF location = layer.location;
 96		QSizeF layerDims(layer.image.width(), layer.image.height());
 97		QRegion region;
 98		if (layer.repeats) {
 99			if (location.x() + layerDims.width() < 0) {
100				location.setX(std::fmod(location.x(), layerDims.width()));
101			}
102			if (location.y() + layerDims.height() < 0) {
103				location.setY(std::fmod(location.y(), layerDims.height()));
104			}
105
106			region += layer.mask.translated(location.x(), location.y());
107			region += layer.mask.translated(location.x() + layerDims.width(), location.y());
108			region += layer.mask.translated(location.x(), location.y() + layerDims.height());
109			region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height());
110		} else {
111			region = layer.mask.translated(location.x(), location.y());
112		}
113
114		if (region.contains(QPoint(coord.x(), coord.y()))) {
115			out = &layer;
116			return true;
117		}
118	}
119	return false;
120}
121
122void FrameView::selectLayer(const QPointF& coord) {
123	Layer* layer;
124	if (!lookupLayer(coord, layer)) {
125		return;
126	}
127	if (layer->id == m_active) {
128		m_active = {};
129	} else {
130		m_active = layer->id;
131	}
132	m_glowFrame = 0;
133}
134
135void FrameView::disableLayer(const QPointF& coord) {
136	Layer* layer;
137	if (!lookupLayer(coord, layer)) {
138		return;
139	}
140	layer->enabled = false;
141	m_disabled.insert(layer->id);
142}
143
144#ifdef M_CORE_GBA
145void FrameView::updateTilesGBA(bool force) {
146	if (m_ui.freeze->checkState() == Qt::Checked) {
147		return;
148	}
149	QMutexLocker locker(&m_mutex);
150	m_queue.clear();
151	{
152		CoreController::Interrupter interrupter(m_controller);
153
154		uint16_t* io = static_cast<GBA*>(m_controller->thread()->core->board)->memory.io;
155		QRgb backdrop = M_RGB5_TO_RGB8(static_cast<GBA*>(m_controller->thread()->core->board)->video.palette[0]);
156		m_gbaDispcnt = io[REG_DISPCNT >> 1];
157		int mode = GBARegisterDISPCNTGetMode(m_gbaDispcnt);
158
159		std::array<bool, 4> enabled{
160			bool(GBARegisterDISPCNTIsBg0Enable(m_gbaDispcnt)),
161			bool(GBARegisterDISPCNTIsBg1Enable(m_gbaDispcnt)),
162			bool(GBARegisterDISPCNTIsBg2Enable(m_gbaDispcnt)),
163			bool(GBARegisterDISPCNTIsBg3Enable(m_gbaDispcnt)),
164		};
165
166		for (int priority = 0; priority < 4; ++priority) {
167			for (int sprite = 0; sprite < 128; ++sprite) {
168				ObjInfo info;
169				lookupObj(sprite, &info);
170
171				if (!info.enabled || info.priority != priority) {
172					continue;
173				}
174
175				QPointF offset(info.x, info.y);
176				QImage obj(compositeObj(info));
177				if (info.hflip || info.vflip) {
178					obj = obj.mirrored(info.hflip, info.vflip);
179				}
180				if (!info.xform.isIdentity()) {
181					offset += QPointF(obj.width(), obj.height()) / 2;
182					obj = obj.transformed(info.xform);
183					offset -= QPointF(obj.width() / 2, obj.height() / 2);
184				}
185				m_queue.append({
186					{ LayerId::SPRITE, sprite },
187					!m_disabled.contains({ LayerId::SPRITE, sprite }),
188					QPixmap::fromImage(obj),
189					{}, offset, false
190				});
191				if (m_queue.back().image.hasAlpha()) {
192					m_queue.back().mask = QRegion(m_queue.back().image.mask());
193				} else {
194					m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
195				}
196			}
197
198			for (int bg = 0; bg < 4; ++bg) {
199				if (!enabled[bg]) {
200					continue;
201				}
202				if (GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + bg]) != priority) {
203					continue;
204				}
205
206				QPointF offset;
207				if (mode == 0) {
208					offset.setX(-(io[(REG_BG0HOFS >> 1) + (bg << 1)] & 0x1FF));
209					offset.setY(-(io[(REG_BG0VOFS >> 1) + (bg << 1)] & 0x1FF));
210				};
211				m_queue.append({
212					{ LayerId::BACKGROUND, bg },
213					!m_disabled.contains({ LayerId::BACKGROUND, bg }),
214					QPixmap::fromImage(compositeMap(bg, m_mapStatus[bg])),
215					{}, offset, true
216				});
217				if (m_queue.back().image.hasAlpha()) {
218					m_queue.back().mask = QRegion(m_queue.back().image.mask());
219				} else {
220					m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
221				}
222			}
223		}
224		QImage backdropImage(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS), QImage::Format_Mono);
225		backdropImage.fill(1);
226		backdropImage.setColorTable({backdrop, backdrop | 0xFF000000 });
227		m_queue.append({
228			{ LayerId::BACKDROP },
229			!m_disabled.contains({ LayerId::BACKDROP }),
230			QPixmap::fromImage(backdropImage),
231			{}, {0, 0}, false
232		});
233		updateRendered();
234	}
235	invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS));
236}
237
238void FrameView::injectGBA() {
239	mVideoLogger* logger = m_vl->videoLogger;
240	mVideoLoggerInjectionPoint(logger, LOGGER_INJECTION_FIRST_SCANLINE);
241	GBA* gba = static_cast<GBA*>(m_vl->board);
242	gba->video.renderer->highlightBG[0] = false;
243	gba->video.renderer->highlightBG[1] = false;
244	gba->video.renderer->highlightBG[2] = false;
245	gba->video.renderer->highlightBG[3] = false;
246	for (int i = 0; i < 128; ++i) {
247		gba->video.renderer->highlightOBJ[i] = false;
248	}
249	QPalette palette;
250	gba->video.renderer->highlightColor = palette.color(QPalette::HighlightedText).rgb();
251	gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 48 + 64;
252	if (!m_overrideBackdrop.isValid()) {
253		QRgb backdrop = M_RGB5_TO_RGB8(gba->video.palette[0]) | 0xFF000000;
254		m_backdropPicker.setColor(backdrop);
255	}
256
257	m_vl->reset(m_vl);
258	for (const Layer& layer : m_queue) {
259		switch (layer.id.type) {
260		case LayerId::SPRITE:
261			if (!layer.enabled) {
262				mVideoLoggerInjectOAM(logger, layer.id.index << 2, 0x200);
263			}
264			if (layer.id == m_active) {
265				gba->video.renderer->highlightOBJ[layer.id.index] = true;
266			}
267			break;
268		case LayerId::BACKGROUND:
269			m_vl->enableVideoLayer(m_vl, layer.id.index, layer.enabled);
270			if (layer.id == m_active) {
271				gba->video.renderer->highlightBG[layer.id.index] = true;
272			}
273			break;
274		}
275	}
276	if (m_overrideBackdrop.isValid()) {
277		mVideoLoggerInjectPalette(logger, 0, M_RGB8_TO_RGB5(m_overrideBackdrop.rgb()));
278	}
279	if (m_ui.disableScanline->checkState() == Qt::Checked) {
280		mVideoLoggerIgnoreAfterInjection(logger, (1 << DIRTY_PALETTE) | (1 << DIRTY_OAM) | (1 << DIRTY_REGISTER));
281	} else {
282		mVideoLoggerIgnoreAfterInjection(logger, 0);
283	}
284}
285#endif
286
287#ifdef M_CORE_GB
288void FrameView::updateTilesGB(bool force) {
289	if (m_ui.freeze->checkState() == Qt::Checked) {
290		return;
291	}
292	m_queue.clear();
293	{
294		CoreController::Interrupter interrupter(m_controller);
295		updateRendered();
296	}
297	invalidateQueue(m_controller->screenDimensions());
298}
299
300void FrameView::injectGB() {
301	for (const Layer& layer : m_queue) {
302	}
303}
304#endif
305
306void FrameView::invalidateQueue(const QSize& dims) {
307	if (dims.isValid()) {
308		m_dims = dims;
309	}
310	bool blockSignals = m_ui.queue->blockSignals(true);
311	QMutexLocker locker(&m_mutex);
312	if (m_vl) {
313		switch (m_controller->platform()) {
314#ifdef M_CORE_GBA
315		case PLATFORM_GBA:
316			injectGBA();
317#endif
318#ifdef M_CORE_GB
319		case PLATFORM_GB:
320			injectGB();
321#endif
322		}
323		m_vl->runFrame(m_vl);
324	}
325
326	for (int i = 0; i < m_queue.count(); ++i) {
327		const Layer& layer = m_queue[i];
328		QListWidgetItem* item;
329		if (i >= m_ui.queue->count()) {
330			item = new QListWidgetItem;
331			m_ui.queue->addItem(item);
332		} else {
333			item = m_ui.queue->item(i);
334		}
335		item->setText(layer.id.readable());
336		item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
337		item->setCheckState(layer.enabled ? Qt::Checked : Qt::Unchecked);
338		item->setData(Qt::UserRole, i);
339		item->setSelected(layer.id == m_active);
340	}
341
342	while (m_ui.queue->count() > m_queue.count()) {
343		delete m_ui.queue->takeItem(m_queue.count());
344	}
345	m_ui.queue->blockSignals(blockSignals);
346
347	QPixmap composited;
348	if (m_framebuffer.isNull()) {
349		updateRendered();
350		composited = m_rendered;
351	} else {
352		m_ui.exportButton->setEnabled(true);
353		composited.convertFromImage(m_framebuffer);
354	}
355	m_composited = composited.scaled(m_dims * m_ui.magnification->value());
356	m_ui.compositedView->setPixmap(m_composited);
357}
358
359void FrameView::updateRendered() {
360	if (m_ui.freeze->checkState() == Qt::Checked) {
361		return;
362	}
363	m_rendered.convertFromImage(m_controller->getPixels());
364}
365
366bool FrameView::eventFilter(QObject* obj, QEvent* event) {
367	QPointF pos;
368	switch (event->type()) {
369	case QEvent::MouseButtonPress:
370		pos = static_cast<QMouseEvent*>(event)->localPos();
371		pos /= m_ui.magnification->value();
372		selectLayer(pos);
373		return true;
374	case QEvent::MouseButtonDblClick:
375		pos = static_cast<QMouseEvent*>(event)->localPos();
376		pos /= m_ui.magnification->value();
377		disableLayer(pos);
378		return true;
379	}
380	return false;
381}
382
383void FrameView::refreshVl() {
384	QMutexLocker locker(&m_mutex);
385	m_currentFrame = m_nextFrame;
386	m_nextFrame = VFileMemChunk(nullptr, 0);
387	if (m_currentFrame) {
388		m_controller->endVideoLog(false);
389		QMetaObject::invokeMethod(this, "newVl");
390	}
391	m_controller->endVideoLog();
392	m_controller->startVideoLog(m_nextFrame, false);
393}
394
395void FrameView::newVl() {
396	if (!m_glowTimer.isActive()) {
397		m_glowTimer.start();
398	}
399	QMutexLocker locker(&m_mutex);
400	if (!m_currentFrame) {
401		return;
402	}
403	if (m_vl) {
404		m_vl->deinit(m_vl);
405	}
406	m_vl = mCoreFindVF(m_currentFrame);
407	m_vl->init(m_vl);
408	m_vl->loadROM(m_vl, m_currentFrame);
409	m_currentFrame = nullptr;
410	mCoreInitConfig(m_vl, nullptr);
411	unsigned width, height;
412	m_vl->desiredVideoDimensions(m_vl, &width, &height);
413	m_framebuffer = QImage(width, height, QImage::Format_RGBX8888);
414	m_vl->setVideoBuffer(m_vl, reinterpret_cast<color_t*>(m_framebuffer.bits()), width);
415	m_vl->reset(m_vl);
416}
417
418void FrameView::frameCallback(FrameView* viewer, std::shared_ptr<bool> lock) {
419	if (!*lock) {
420		return;
421	}
422	CoreController::Interrupter interrupter(viewer->m_controller, true);
423	viewer->refreshVl();
424	viewer->m_controller->addFrameAction(std::bind(&FrameView::frameCallback, viewer, lock));
425}
426
427void FrameView::exportFrame() {
428	QString filename = GBAApp::app()->getSaveFileName(this, tr("Export frame"),
429	                                                  tr("Portable Network Graphics (*.png)"));
430	CoreController::Interrupter interrupter(m_controller);
431	m_framebuffer.save(filename, "PNG");
432}
433
434void FrameView::reset() {
435	m_disabled.clear();
436	for (Layer& layer : m_queue) {
437		layer.enabled = true;
438	}
439	m_overrideBackdrop = QColor();
440	invalidateQueue();
441}
442
443QString FrameView::LayerId::readable() const {
444	QString typeStr;
445	switch (type) {
446	case NONE:
447		return tr("None");
448	case BACKGROUND:
449		typeStr = tr("Background");
450		break;
451	case WINDOW:
452		typeStr = tr("Window");
453		break;
454	case SPRITE:
455		typeStr = tr("Sprite");
456		break;
457	case BACKDROP:
458		typeStr = tr("Backdrop");
459		break;
460	}
461	if (index < 0) {
462		return typeStr;
463	}
464	return tr("%1 %2").arg(typeStr).arg(index);
465}