mgba/src/platform/qt/FrameView.cpp

467 lines
13 KiB
C++

/* Copyright (c) 2013-2019 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "FrameView.h"
#include <QMouseEvent>
#include <QPalette>
#include <array>
#include <cmath>
#include "CoreController.h"
#include "GBAApp.h"
#include <mgba/core/core.h>
#include <mgba/feature/video-logger.h>
#ifdef M_CORE_GBA
#include <mgba/internal/gba/gba.h>
#include <mgba/internal/gba/io.h>
#include <mgba/internal/gba/memory.h>
#include <mgba/internal/gba/video.h>
#endif
#ifdef M_CORE_GB
#include <mgba/internal/gb/gb.h>
#include <mgba/internal/gb/memory.h>
#endif
using namespace QGBA;
FrameView::FrameView(std::shared_ptr<CoreController> controller, QWidget* parent)
: AssetView(controller, parent)
{
m_ui.setupUi(this);
m_glowTimer.setInterval(33);
connect(&m_glowTimer, &QTimer::timeout, this, [this]() {
++m_glowFrame;
invalidateQueue();
});
m_ui.compositedView->installEventFilter(this);
connect(m_ui.queue, &QListWidget::itemChanged, this, [this](QListWidgetItem* item) {
Layer& layer = m_queue[item->data(Qt::UserRole).toInt()];
layer.enabled = item->checkState() == Qt::Checked;
if (layer.enabled) {
m_disabled.remove(layer.id);
} else {
m_disabled.insert(layer.id);
}
invalidateQueue();
});
connect(m_ui.queue, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* item) {
if (item) {
m_active = m_queue[item->data(Qt::UserRole).toInt()].id;
} else {
m_active = {};
}
invalidateQueue();
});
connect(m_ui.magnification, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]() {
invalidateQueue();
});
connect(m_ui.exportButton, &QAbstractButton::pressed, this, &FrameView::exportFrame);
connect(m_ui.reset, &QAbstractButton::pressed, this, &FrameView::reset);
m_backdropPicker = ColorPicker(m_ui.backdrop, QColor(0, 0, 0, 0));
connect(&m_backdropPicker, &ColorPicker::colorChanged, this, [this](const QColor& color) {
m_overrideBackdrop = color;
});
m_controller->addFrameAction(std::bind(&FrameView::frameCallback, this, m_callbackLocker));
{
CoreController::Interrupter interrupter(m_controller);
refreshVl();
}
m_controller->frameAdvance();
}
FrameView::~FrameView() {
QMutexLocker locker(&m_mutex);
*m_callbackLocker = false;
if (m_vl) {
m_vl->deinit(m_vl);
}
}
bool FrameView::lookupLayer(const QPointF& coord, Layer*& out) {
for (Layer& layer : m_queue) {
if (!layer.enabled || m_disabled.contains(layer.id)) {
continue;
}
QPointF location = layer.location;
QSizeF layerDims(layer.image.width(), layer.image.height());
QRegion region;
if (layer.repeats) {
if (location.x() + layerDims.width() < 0) {
location.setX(std::fmod(location.x(), layerDims.width()));
}
if (location.y() + layerDims.height() < 0) {
location.setY(std::fmod(location.y(), layerDims.height()));
}
region += layer.mask.translated(location.x(), location.y());
region += layer.mask.translated(location.x() + layerDims.width(), location.y());
region += layer.mask.translated(location.x(), location.y() + layerDims.height());
region += layer.mask.translated(location.x() + layerDims.width(), location.y() + layerDims.height());
} else {
region = layer.mask.translated(location.x(), location.y());
}
if (region.contains(QPoint(coord.x(), coord.y()))) {
out = &layer;
return true;
}
}
return false;
}
void FrameView::selectLayer(const QPointF& coord) {
Layer* layer;
if (!lookupLayer(coord, layer)) {
return;
}
if (layer->id == m_active) {
m_active = {};
} else {
m_active = layer->id;
}
m_glowFrame = 0;
}
void FrameView::disableLayer(const QPointF& coord) {
Layer* layer;
if (!lookupLayer(coord, layer)) {
return;
}
layer->enabled = false;
m_disabled.insert(layer->id);
}
#ifdef M_CORE_GBA
void FrameView::updateTilesGBA(bool force) {
if (m_ui.freeze->checkState() == Qt::Checked) {
return;
}
QMutexLocker locker(&m_mutex);
m_queue.clear();
{
CoreController::Interrupter interrupter(m_controller);
uint16_t* io = static_cast<GBA*>(m_controller->thread()->core->board)->memory.io;
QRgb backdrop = M_RGB5_TO_RGB8(static_cast<GBA*>(m_controller->thread()->core->board)->video.palette[0]);
m_gbaDispcnt = io[REG_DISPCNT >> 1];
int mode = GBARegisterDISPCNTGetMode(m_gbaDispcnt);
std::array<bool, 4> enabled{
bool(GBARegisterDISPCNTIsBg0Enable(m_gbaDispcnt)),
bool(GBARegisterDISPCNTIsBg1Enable(m_gbaDispcnt)),
bool(GBARegisterDISPCNTIsBg2Enable(m_gbaDispcnt)),
bool(GBARegisterDISPCNTIsBg3Enable(m_gbaDispcnt)),
};
for (int priority = 0; priority < 4; ++priority) {
for (int sprite = 0; sprite < 128; ++sprite) {
ObjInfo info;
lookupObj(sprite, &info);
if (!info.enabled || info.priority != priority) {
continue;
}
QPointF offset(info.x, info.y);
QImage obj(compositeObj(info));
if (info.hflip || info.vflip) {
obj = obj.mirrored(info.hflip, info.vflip);
}
if (!info.xform.isIdentity()) {
offset += QPointF(obj.width(), obj.height()) / 2;
obj = obj.transformed(info.xform);
offset -= QPointF(obj.width() / 2, obj.height() / 2);
}
m_queue.append({
{ LayerId::SPRITE, sprite },
!m_disabled.contains({ LayerId::SPRITE, sprite }),
QPixmap::fromImage(obj),
{}, offset, false
});
if (m_queue.back().image.hasAlpha()) {
m_queue.back().mask = QRegion(m_queue.back().image.mask());
} else {
m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
}
}
for (int bg = 0; bg < 4; ++bg) {
if (!enabled[bg]) {
continue;
}
if (GBARegisterBGCNTGetPriority(io[(REG_BG0CNT >> 1) + bg]) != priority) {
continue;
}
QPointF offset;
if (mode == 0) {
offset.setX(-(io[(REG_BG0HOFS >> 1) + (bg << 1)] & 0x1FF));
offset.setY(-(io[(REG_BG0VOFS >> 1) + (bg << 1)] & 0x1FF));
};
m_queue.append({
{ LayerId::BACKGROUND, bg },
!m_disabled.contains({ LayerId::BACKGROUND, bg }),
QPixmap::fromImage(compositeMap(bg, m_mapStatus[bg])),
{}, offset, true
});
if (m_queue.back().image.hasAlpha()) {
m_queue.back().mask = QRegion(m_queue.back().image.mask());
} else {
m_queue.back().mask = QRegion(0, 0, m_queue.back().image.width(), m_queue.back().image.height());
}
}
}
QImage backdropImage(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS), QImage::Format_Mono);
backdropImage.fill(1);
backdropImage.setColorTable({backdrop, backdrop | 0xFF000000 });
m_queue.append({
{ LayerId::BACKDROP },
!m_disabled.contains({ LayerId::BACKDROP }),
QPixmap::fromImage(backdropImage),
{}, {0, 0}, false
});
updateRendered();
}
invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS));
}
void FrameView::injectGBA() {
mVideoLogger* logger = m_vl->videoLogger;
mVideoLoggerInjectionPoint(logger, LOGGER_INJECTION_FIRST_SCANLINE);
GBA* gba = static_cast<GBA*>(m_vl->board);
gba->video.renderer->highlightBG[0] = false;
gba->video.renderer->highlightBG[1] = false;
gba->video.renderer->highlightBG[2] = false;
gba->video.renderer->highlightBG[3] = false;
for (int i = 0; i < 128; ++i) {
gba->video.renderer->highlightOBJ[i] = false;
}
QPalette palette;
gba->video.renderer->highlightColor = palette.color(QPalette::HighlightedText).rgb();
gba->video.renderer->highlightAmount = sin(m_glowFrame * M_PI / 30) * 48 + 64;
if (!m_overrideBackdrop.isValid()) {
QRgb backdrop = M_RGB5_TO_RGB8(gba->video.palette[0]) | 0xFF000000;
m_backdropPicker.setColor(backdrop);
}
m_vl->reset(m_vl);
for (const Layer& layer : m_queue) {
switch (layer.id.type) {
case LayerId::SPRITE:
if (!layer.enabled) {
mVideoLoggerInjectOAM(logger, layer.id.index << 2, 0x200);
}
if (layer.id == m_active) {
gba->video.renderer->highlightOBJ[layer.id.index] = true;
}
break;
case LayerId::BACKGROUND:
m_vl->enableVideoLayer(m_vl, layer.id.index, layer.enabled);
if (layer.id == m_active) {
gba->video.renderer->highlightBG[layer.id.index] = true;
}
break;
}
}
if (m_overrideBackdrop.isValid()) {
mVideoLoggerInjectPalette(logger, 0, M_RGB8_TO_RGB5(m_overrideBackdrop.rgb()));
}
if (m_ui.disableScanline->checkState() == Qt::Checked) {
mVideoLoggerIgnoreAfterInjection(logger, (1 << DIRTY_PALETTE) | (1 << DIRTY_OAM) | (1 << DIRTY_REGISTER));
} else {
mVideoLoggerIgnoreAfterInjection(logger, 0);
}
}
#endif
#ifdef M_CORE_GB
void FrameView::updateTilesGB(bool force) {
if (m_ui.freeze->checkState() == Qt::Checked) {
return;
}
m_queue.clear();
{
CoreController::Interrupter interrupter(m_controller);
updateRendered();
}
invalidateQueue(m_controller->screenDimensions());
}
void FrameView::injectGB() {
for (const Layer& layer : m_queue) {
}
}
#endif
void FrameView::invalidateQueue(const QSize& dims) {
if (dims.isValid()) {
m_dims = dims;
}
bool blockSignals = m_ui.queue->blockSignals(true);
QMutexLocker locker(&m_mutex);
if (m_vl) {
switch (m_controller->platform()) {
#ifdef M_CORE_GBA
case PLATFORM_GBA:
injectGBA();
#endif
#ifdef M_CORE_GB
case PLATFORM_GB:
injectGB();
#endif
}
m_vl->runFrame(m_vl);
}
for (int i = 0; i < m_queue.count(); ++i) {
const Layer& layer = m_queue[i];
QListWidgetItem* item;
if (i >= m_ui.queue->count()) {
item = new QListWidgetItem;
m_ui.queue->addItem(item);
} else {
item = m_ui.queue->item(i);
}
item->setText(layer.id.readable());
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
item->setCheckState(layer.enabled ? Qt::Checked : Qt::Unchecked);
item->setData(Qt::UserRole, i);
item->setSelected(layer.id == m_active);
}
while (m_ui.queue->count() > m_queue.count()) {
delete m_ui.queue->takeItem(m_queue.count());
}
m_ui.queue->blockSignals(blockSignals);
QPixmap composited;
if (m_framebuffer.isNull()) {
updateRendered();
composited = m_rendered;
} else {
m_ui.exportButton->setEnabled(true);
composited.convertFromImage(m_framebuffer);
}
m_composited = composited.scaled(m_dims * m_ui.magnification->value());
m_ui.compositedView->setPixmap(m_composited);
}
void FrameView::updateRendered() {
if (m_ui.freeze->checkState() == Qt::Checked) {
return;
}
m_rendered.convertFromImage(m_controller->getPixels());
}
bool FrameView::eventFilter(QObject*, QEvent* event) {
QPointF pos;
switch (event->type()) {
case QEvent::MouseButtonPress:
pos = static_cast<QMouseEvent*>(event)->localPos();
pos /= m_ui.magnification->value();
selectLayer(pos);
return true;
case QEvent::MouseButtonDblClick:
pos = static_cast<QMouseEvent*>(event)->localPos();
pos /= m_ui.magnification->value();
disableLayer(pos);
return true;
}
return false;
}
void FrameView::refreshVl() {
QMutexLocker locker(&m_mutex);
m_currentFrame = m_nextFrame;
m_nextFrame = VFileMemChunk(nullptr, 0);
if (m_currentFrame) {
m_controller->endVideoLog(false);
VFile* currentFrame = VFileMemChunk(nullptr, m_currentFrame->size(m_currentFrame));
void* buffer = currentFrame->map(currentFrame, m_currentFrame->size(m_currentFrame), MAP_WRITE);
m_currentFrame->seek(m_currentFrame, 0, SEEK_SET);
m_currentFrame->read(m_currentFrame, buffer, m_currentFrame->size(m_currentFrame));
currentFrame->unmap(currentFrame, buffer, m_currentFrame->size(m_currentFrame));
m_currentFrame = currentFrame;
QMetaObject::invokeMethod(this, "newVl");
}
m_controller->endVideoLog();
m_controller->startVideoLog(m_nextFrame, false);
}
void FrameView::newVl() {
if (!m_glowTimer.isActive()) {
m_glowTimer.start();
}
QMutexLocker locker(&m_mutex);
if (m_vl) {
m_vl->deinit(m_vl);
}
m_vl = mCoreFindVF(m_currentFrame);
m_vl->init(m_vl);
m_vl->loadROM(m_vl, m_currentFrame);
mCoreInitConfig(m_vl, nullptr);
unsigned width, height;
m_vl->desiredVideoDimensions(m_vl, &width, &height);
m_framebuffer = QImage(width, height, QImage::Format_RGBX8888);
m_vl->setVideoBuffer(m_vl, reinterpret_cast<color_t*>(m_framebuffer.bits()), width);
m_vl->reset(m_vl);
}
void FrameView::frameCallback(FrameView* viewer, std::shared_ptr<bool> lock) {
if (!*lock) {
return;
}
CoreController::Interrupter interrupter(viewer->m_controller, true);
viewer->refreshVl();
viewer->m_controller->addFrameAction(std::bind(&FrameView::frameCallback, viewer, lock));
}
void FrameView::exportFrame() {
QString filename = GBAApp::app()->getSaveFileName(this, tr("Export frame"),
tr("Portable Network Graphics (*.png)"));
CoreController::Interrupter interrupter(m_controller);
m_framebuffer.save(filename, "PNG");
}
void FrameView::reset() {
m_disabled.clear();
for (Layer& layer : m_queue) {
layer.enabled = true;
}
m_overrideBackdrop = QColor();
invalidateQueue();
}
QString FrameView::LayerId::readable() const {
QString typeStr;
switch (type) {
case NONE:
return tr("None");
case BACKGROUND:
typeStr = tr("Background");
break;
case WINDOW:
typeStr = tr("Window");
break;
case SPRITE:
typeStr = tr("Sprite");
break;
case BACKDROP:
typeStr = tr("Backdrop");
break;
}
if (index < 0) {
return typeStr;
}
return tr("%1 %2").arg(typeStr).arg(index);
}