all repos — mgba @ b99d8164ddba2e6f8fc16de941d1551674974e4c

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