all repos — mgba @ ef3cc7bd9f28d73fdff912587fc42260948d5b51

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