Qt: Add frame inspector for GBA games

This commit is contained in:
Vicki Pfau 2019-05-31 15:32:22 -07:00
parent db2b56f418
commit 86efc6cc9f
12 changed files with 829 additions and 176 deletions

View File

@ -11,6 +11,7 @@ Features:
- OpenGL renderer with high-resolution upscaling support
- Experimental high level "XQ" audio for most GBA games
- Interframe blending for games that use flicker effects
- Frame inspector for dissecting and debugging rendering
Emulation fixes:
- GBA: All IRQs have 7 cycle delay (fixes mgba.io/i/539, mgba.io/i/1208)
- GBA: Reset now reloads multiboot ROMs

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2013-2016 Jeffrey Pfau
/* 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
@ -9,6 +9,16 @@
#include <QTimer>
#ifdef M_CORE_GBA
#include <mgba/internal/gba/gba.h>
#endif
#ifdef M_CORE_GB
#include <mgba/internal/gb/gb.h>
#include <mgba/internal/gb/io.h>
#endif
#include <mgba/core/map-cache.h>
using namespace QGBA;
AssetView::AssetView(std::shared_ptr<CoreController> controller, QWidget* parent)
@ -98,3 +108,166 @@ void AssetView::compositeTile(const void* tBuffer, void* buffer, size_t stride,
break;
}
}
QImage AssetView::compositeMap(int map, mMapCacheEntry* mapStatus) {
mMapCache* mapCache = mMapCacheSetGetPointer(&m_cacheSet->maps, map);
int tilesW = 1 << mMapCacheSystemInfoGetTilesWide(mapCache->sysConfig);
int tilesH = 1 << mMapCacheSystemInfoGetTilesHigh(mapCache->sysConfig);
QImage rawMap = QImage(QSize(tilesW * 8, tilesH * 8), QImage::Format_ARGB32);
uchar* bgBits = rawMap.bits();
for (int j = 0; j < tilesH; ++j) {
for (int i = 0; i < tilesW; ++i) {
mMapCacheCleanTile(mapCache, mapStatus, i, j);
}
for (int i = 0; i < 8; ++i) {
memcpy(static_cast<void*>(&bgBits[tilesW * 32 * (i + j * 8)]), mMapCacheGetRow(mapCache, i + j * 8), tilesW * 32);
}
}
return rawMap.rgbSwapped();
}
QImage AssetView::compositeObj(const ObjInfo& objInfo) {
mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, objInfo.paletteSet);
const color_t* rawPalette = mTileCacheGetPalette(tileCache, objInfo.paletteId);
unsigned colors = 1 << objInfo.bits;
QVector<QRgb> palette;
palette.append(rawPalette[0] & 0xFFFFFF);
for (unsigned c = 1; c < colors && c < 256; ++c) {
palette.append(rawPalette[c] | 0xFF000000);
}
QImage image = QImage(QSize(objInfo.width * 8, objInfo.height * 8), QImage::Format_Indexed8);
image.setColorTable(palette);
uchar* bits = image.bits();
unsigned t = objInfo.tile;
for (int y = 0; y < objInfo.height; ++y) {
for (int x = 0; x < objInfo.width; ++x, ++t) {
compositeTile(static_cast<const void*>(mTileCacheGetVRAM(tileCache, t)), bits, objInfo.width * 8, x * 8, y * 8, objInfo.bits);
}
t += objInfo.stride - objInfo.width;
}
return image.rgbSwapped();
}
bool AssetView::lookupObj(int id, struct ObjInfo* info) {
switch (m_controller->platform()) {
#ifdef M_CORE_GBA
case PLATFORM_GBA:
return lookupObjGBA(id, info);
#endif
#ifdef M_CORE_GB
case PLATFORM_GB:
return lookupObjGB(id, info);
#endif
default:
return false;
}
}
#ifdef M_CORE_GBA
bool AssetView::lookupObjGBA(int id, struct ObjInfo* info) {
if (id > 127) {
return false;
}
const GBA* gba = static_cast<const GBA*>(m_controller->thread()->core->board);
const GBAObj* obj = &gba->video.oam.obj[id];
unsigned shape = GBAObjAttributesAGetShape(obj->a);
unsigned size = GBAObjAttributesBGetSize(obj->b);
unsigned width = GBAVideoObjSizes[shape * 4 + size][0];
unsigned height = GBAVideoObjSizes[shape * 4 + size][1];
unsigned tile = GBAObjAttributesCGetTile(obj->c);
unsigned palette = GBAObjAttributesCGetPalette(obj->c);
unsigned tileBase = tile;
unsigned paletteSet;
unsigned bits;
if (GBAObjAttributesAIs256Color(obj->a)) {
paletteSet = 3;
palette = 0;
tile /= 2;
bits = 8;
} else {
paletteSet = 2;
bits = 4;
}
ObjInfo newInfo{
tile,
width / 8,
height / 8,
width / 8,
palette,
paletteSet,
bits,
!GBAObjAttributesAIsDisable(obj->a) || GBAObjAttributesAIsTransformed(obj->a),
GBAObjAttributesCGetPriority(obj->c),
GBAObjAttributesBGetX(obj->b),
GBAObjAttributesAGetY(obj->a),
GBAObjAttributesBIsHFlip(obj->b),
GBAObjAttributesBIsVFlip(obj->b),
};
GBARegisterDISPCNT dispcnt = gba->memory.io[0]; // FIXME: Register name can't be imported due to namespacing issues
if (!GBARegisterDISPCNTIsObjCharacterMapping(dispcnt)) {
newInfo.stride = 0x20 >> (GBAObjAttributesAGet256Color(obj->a));
};
*info = newInfo;
return true;
}
#endif
#ifdef M_CORE_GB
bool AssetView::lookupObjGB(int id, struct ObjInfo* info) {
if (id > 39) {
return false;
}
const GB* gb = static_cast<const GB*>(m_controller->thread()->core->board);
const GBObj* obj = &gb->video.oam.obj[id];
unsigned width = 8;
unsigned height = 8;
GBRegisterLCDC lcdc = gb->memory.io[REG_LCDC];
if (GBRegisterLCDCIsObjSize(lcdc)) {
height = 16;
}
unsigned tile = obj->tile;
unsigned palette = 0;
if (gb->model >= GB_MODEL_CGB) {
if (GBObjAttributesIsBank(obj->attr)) {
tile += 512;
}
palette = GBObjAttributesGetCGBPalette(obj->attr);
} else {
palette = GBObjAttributesGetPalette(obj->attr);
}
palette += 8;
ObjInfo newInfo{
tile,
1,
height / 8,
1,
palette,
0,
2,
obj->y != 0 && obj->y < 160,
GBObjAttributesGetPriority(obj->attr),
obj->x,
obj->y,
GBObjAttributesIsXFlip(obj->attr),
GBObjAttributesIsYFlip(obj->attr),
};
*info = newInfo;
return true;
}
#endif
bool AssetView::ObjInfo::operator!=(const ObjInfo& other) const {
return other.tile != tile ||
other.width != width ||
other.height != height ||
other.stride != stride ||
other.paletteId != paletteId ||
other.paletteSet != paletteSet;
}

View File

@ -12,6 +12,8 @@
#include <memory>
struct mMapCacheEntry;
namespace QGBA {
class CoreController;
@ -22,8 +24,6 @@ Q_OBJECT
public:
AssetView(std::shared_ptr<CoreController> controller, QWidget* parent = nullptr);
static void compositeTile(const void* tile, void* image, size_t stride, size_t x, size_t y, int depth = 8);
protected slots:
void updateTiles();
void updateTiles(bool force);
@ -40,9 +40,42 @@ protected:
void showEvent(QShowEvent*) override;
mCacheSet* const m_cacheSet;
std::shared_ptr<CoreController> m_controller;
protected:
struct ObjInfo {
unsigned tile;
unsigned width;
unsigned height;
unsigned stride;
unsigned paletteId;
unsigned paletteSet;
unsigned bits;
bool enabled : 1;
unsigned priority : 2;
unsigned x : 9;
unsigned y : 9;
bool hflip : 1;
bool vflip : 1;
bool operator!=(const ObjInfo&) const;
};
static void compositeTile(const void* tile, void* image, size_t stride, size_t x, size_t y, int depth = 8);
QImage compositeMap(int map, mMapCacheEntry*);
QImage compositeObj(const ObjInfo&);
bool lookupObj(int id, struct ObjInfo*);
private:
std::shared_ptr<CoreController> m_controller;
#ifdef M_CORE_GBA
bool lookupObjGBA(int id, struct ObjInfo*);
#endif
#ifdef M_CORE_GB
bool lookupObjGB(int id, struct ObjInfo*);
#endif
QTimer m_updateTimer;
};

View File

@ -72,6 +72,7 @@ set(SOURCE_FILES
Display.cpp
DisplayGL.cpp
DisplayQt.cpp
FrameView.cpp
GBAApp.cpp
GBAKeyEditor.cpp
GIFView.cpp
@ -122,6 +123,7 @@ set(UI_FILES
BattleChipView.ui
CheatsView.ui
DebuggerConsole.ui
FrameView.ui
GIFView.ui
IOViewer.ui
LoadSaveState.ui

View File

@ -0,0 +1,309 @@
/* 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 <QPainter>
#include <QPalette>
#include <array>
#include "CoreController.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_glowTimer.start();
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();
});
}
void FrameView::selectLayer(const QPointF& coord) {
for (const Layer& layer : m_queue) {
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()))) {
m_active = layer.id;
m_glowFrame = 0;
break;
}
}
}
void FrameView::updateTilesGBA(bool force) {
if (m_ui.freeze->checkState() == Qt::Checked) {
return;
}
m_queue.clear();
{
CoreController::Interrupter interrupter(m_controller);
updateRendered();
uint16_t* io = static_cast<GBA*>(m_controller->thread()->core->board)->memory.io;
int mode = GBARegisterDISPCNTGetMode(io[REG_DISPCNT >> 1]);
std::array<bool, 4> enabled{
GBARegisterDISPCNTIsBg0Enable(io[REG_DISPCNT >> 1]),
GBARegisterDISPCNTIsBg1Enable(io[REG_DISPCNT >> 1]),
GBARegisterDISPCNTIsBg2Enable(io[REG_DISPCNT >> 1]),
GBARegisterDISPCNTIsBg3Enable(io[REG_DISPCNT >> 1]),
};
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);
}
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());
}
}
}
}
invalidateQueue(QSize(GBA_VIDEO_HORIZONTAL_PIXELS, GBA_VIDEO_VERTICAL_PIXELS));
}
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::invalidateQueue(const QSize& dims) {
QSize realDims = dims;
if (!dims.isValid()) {
realDims = m_composited.size() / m_ui.magnification->value();
}
bool blockSignals = m_ui.queue->blockSignals(true);
QPixmap composited(realDims);
QPainter painter(&composited);
QPalette palette;
QColor activeColor = palette.color(QPalette::HighlightedText);
activeColor.setAlpha(sin(m_glowFrame * M_PI / 60) * 16 + 96);
QRectF rect(0, 0, realDims.width(), realDims.height());
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.fillRect(rect, QColor(0, 0, 0, 0));
painter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
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);
if (!layer.enabled) {
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()));
}
if (layer.id == m_active) {
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 {
QRectF layerRect(location, layerDims);
if (!rect.intersects(layerRect)) {
continue;
}
if (layer.id == m_active) {
region = layer.mask.translated(location.x(), location.y());
}
}
if (layer.id == m_active) {
painter.setClipping(true);
painter.setClipRegion(region);
painter.fillRect(rect, activeColor);
painter.setClipping(false);
}
if (layer.repeats) {
painter.drawPixmap(location, layer.image);
painter.drawPixmap(location + QPointF(layerDims.width(), 0), layer.image);
painter.drawPixmap(location + QPointF(0, layerDims.height()), layer.image);
painter.drawPixmap(location + QPointF(layerDims.width(), layerDims.height()), layer.image);
} else {
painter.drawPixmap(location, layer.image);
}
}
painter.end();
while (m_ui.queue->count() > m_queue.count()) {
delete m_ui.queue->takeItem(m_queue.count());
}
m_ui.queue->blockSignals(blockSignals);
m_composited = composited.scaled(realDims * 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());
m_rendered = m_rendered.scaledToHeight(m_rendered.height() * m_ui.magnification->value());
m_ui.renderedView->setPixmap(m_rendered);
}
bool FrameView::eventFilter(QObject* obj, QEvent* event) {
if (event->type() != QEvent::MouseButtonPress) {
return false;
}
QPointF pos = static_cast<QMouseEvent*>(event)->localPos();
pos /= m_ui.magnification->value();
selectLayer(pos);
return true;
}
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;
}
if (index < 0) {
return typeStr;
}
return tr("%1 %2").arg(typeStr).arg(index);
}

View File

@ -0,0 +1,85 @@
/* 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/. */
#pragma once
#include "ui_FrameView.h"
#include <QBitmap>
#include <QImage>
#include <QList>
#include <QPixmap>
#include <QSet>
#include <QTimer>
#include "AssetView.h"
#include <memory>
namespace QGBA {
class CoreController;
class FrameView : public AssetView {
Q_OBJECT
public:
FrameView(std::shared_ptr<CoreController> controller, QWidget* parent = nullptr);
public slots:
void selectLayer(const QPointF& coord);
protected:
#ifdef M_CORE_GBA
void updateTilesGBA(bool force) override;
#endif
#ifdef M_CORE_GB
void updateTilesGB(bool force) override;
#endif
bool eventFilter(QObject* obj, QEvent* event) override;
private:
struct LayerId {
enum {
NONE = 0,
BACKGROUND,
WINDOW,
SPRITE
} type = NONE;
int index = -1;
bool operator==(const LayerId& other) const { return other.type == type && other.index == index; }
operator uint() const { return (type << 8) | index; }
QString readable() const;
};
struct Layer {
LayerId id;
bool enabled;
QPixmap image;
QRegion mask;
QPointF location;
bool repeats;
};
void invalidateQueue(const QSize& dims = QSize());
void updateRendered();
Ui::FrameView m_ui;
LayerId m_active{};
int m_glowFrame;
QTimer m_glowTimer;
QList<Layer> m_queue;
QSet<LayerId> m_disabled;
QPixmap m_composited;
QPixmap m_rendered;
mMapCacheEntry m_mapStatus[4][128 * 128] = {}; // TODO: Correct size
};
}

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FrameView</class>
<widget class="QWidget" name="FrameView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>869</width>
<height>875</height>
</rect>
</property>
<property name="windowTitle">
<string>Inspect frame</string>
</property>
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,1,0,1,0">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QSpinBox" name="magnification">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string>×</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>8</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Magnification</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="freeze">
<property name="text">
<string>Freeze frame</string>
</property>
</widget>
</item>
<item row="4" column="1" rowspan="2">
<widget class="QScrollArea" name="compositedArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>591</width>
<height>403</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="compositedView">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="0" column="1" rowspan="4">
<widget class="QScrollArea" name="renderedArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>591</width>
<height>446</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="renderedView">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="5" column="0">
<widget class="QPushButton" name="exportButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Export</string>
</property>
</widget>
</item>
<item row="2" column="0" rowspan="3">
<widget class="QListWidget" name="queue">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -211,6 +211,7 @@ void MapView::updateTilesGBA(bool force) {
mBitmapCacheCleanRow(bitmapCache, m_bitmapStatus, j);
memcpy(static_cast<void*>(&bgBits[width * j * 4]), mBitmapCacheGetRow(bitmapCache, j), width * 4);
}
m_rawMap = m_rawMap.rgbSwapped();
} else {
mMapCache* mapCache = mMapCacheSetGetPointer(&m_cacheSet->maps, m_map);
int tilesW = 1 << mMapCacheSystemInfoGetTilesWide(mapCache->sysConfig);
@ -225,19 +226,9 @@ void MapView::updateTilesGBA(bool force) {
m_ui.bgInfo->setCustomProperty("priority", priority);
m_ui.bgInfo->setCustomProperty("offset", offset);
m_ui.bgInfo->setCustomProperty("transform", transform);
m_rawMap = QImage(QSize(tilesW * 8, tilesH * 8), QImage::Format_ARGB32);
uchar* bgBits = m_rawMap.bits();
for (int j = 0; j < tilesH; ++j) {
for (int i = 0; i < tilesW; ++i) {
mMapCacheCleanTile(mapCache, m_mapStatus, i, j);
}
for (int i = 0; i < 8; ++i) {
memcpy(static_cast<void*>(&bgBits[tilesW * 32 * (i + j * 8)]), mMapCacheGetRow(mapCache, i + j * 8), tilesW * 32);
m_rawMap = compositeMap(m_map, m_mapStatus);
}
}
}
}
m_rawMap = m_rawMap.rgbSwapped();
QPixmap map = QPixmap::fromImage(m_rawMap.convertToFormat(QImage::Format_RGB32));
if (m_ui.magnification->value() > 1) {
map = map.scaled(map.size() * m_ui.magnification->value());

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1273</width>
<height>736</height>
<width>941</width>
<height>617</height>
</rect>
</property>
<property name="windowTitle">
@ -92,8 +92,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>835</width>
<height>720</height>
<width>613</width>
<height>601</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">

View File

@ -19,9 +19,7 @@
#endif
#ifdef M_CORE_GB
#include <mgba/internal/gb/gb.h>
#include <mgba/internal/gb/io.h>
#endif
#include <mgba-util/png-io.h>
#include <mgba-util/vfs.h>
using namespace QGBA;
@ -53,11 +51,7 @@ ObjView::ObjView(std::shared_ptr<CoreController> controller, QWidget* parent)
connect(m_ui.magnification, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this]() {
updateTiles(true);
});
#ifdef USE_PNG
connect(m_ui.exportButton, &QAbstractButton::clicked, this, &ObjView::exportObj);
#else
m_ui.exportButton->setVisible(false);
#endif
}
void ObjView::selectObj(int obj) {
@ -77,79 +71,56 @@ void ObjView::updateTilesGBA(bool force) {
const GBA* gba = static_cast<const GBA*>(m_controller->thread()->core->board);
const GBAObj* obj = &gba->video.oam.obj[m_objId];
unsigned shape = GBAObjAttributesAGetShape(obj->a);
unsigned size = GBAObjAttributesBGetSize(obj->b);
unsigned width = GBAVideoObjSizes[shape * 4 + size][0];
unsigned height = GBAVideoObjSizes[shape * 4 + size][1];
unsigned tile = GBAObjAttributesCGetTile(obj->c);
m_ui.tiles->setTileCount(width * height / 64);
m_ui.tiles->setMinimumSize(QSize(width, height) * m_ui.magnification->value());
m_ui.tiles->resize(QSize(width, height) * m_ui.magnification->value());
unsigned palette = GBAObjAttributesCGetPalette(obj->c);
unsigned tileBase = tile;
unsigned paletteSet;
unsigned bits;
ObjInfo newInfo;
lookupObj(m_objId, &newInfo);
m_ui.tiles->setTileCount(newInfo.width * newInfo.height);
m_ui.tiles->setMinimumSize(QSize(newInfo.width * 8, newInfo.height * 8) * m_ui.magnification->value());
m_ui.tiles->resize(QSize(newInfo.width * 8, newInfo.height * 8) * m_ui.magnification->value());
unsigned tileBase = newInfo.tile;
unsigned tile = newInfo.tile;
if (GBAObjAttributesAIs256Color(obj->a)) {
m_ui.palette->setText("256-color");
paletteSet = 3;
m_ui.tile->setBoundary(1024, 1, 3);
m_ui.tile->setPalette(0);
m_boundary = 1024;
palette = 0;
tile /= 2;
bits = 8;
tileBase *= 2;
} else {
m_ui.palette->setText(QString::number(palette));
paletteSet = 2;
m_ui.palette->setText(QString::number(newInfo.paletteId));
m_ui.tile->setBoundary(2048, 0, 2);
m_ui.tile->setPalette(palette);
m_boundary = 2048;
bits = 4;
m_ui.tile->setPalette(newInfo.paletteId);
}
ObjInfo newInfo{
tile,
width / 8,
height / 8,
width / 8,
palette,
paletteSet,
bits
};
if (newInfo != m_objInfo) {
force = true;
}
GBARegisterDISPCNT dispcnt = gba->memory.io[0]; // FIXME: Register name can't be imported due to namespacing issues
if (!GBARegisterDISPCNTIsObjCharacterMapping(dispcnt)) {
newInfo.stride = 0x20 >> (GBAObjAttributesAGet256Color(obj->a));
};
m_objInfo = newInfo;
m_tileOffset = tile;
mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, paletteSet);
m_tileOffset = newInfo.tile;
mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, newInfo.paletteSet);
int i = 0;
for (int y = 0; y < height / 8; ++y) {
for (int x = 0; x < width / 8; ++x, ++i, ++tile, ++tileBase) {
const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[16 * tileBase], tile, palette);
for (int y = 0; y < newInfo.height; ++y) {
for (int x = 0; x < newInfo.width; ++x, ++i, ++tile, ++tileBase) {
const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[16 * tileBase], tile, newInfo.paletteId);
if (data) {
m_ui.tiles->setTile(i, data);
} else if (force) {
m_ui.tiles->setTile(i, mTileCacheGetTile(tileCache, tile, palette));
m_ui.tiles->setTile(i, mTileCacheGetTile(tileCache, tile, newInfo.paletteId));
}
}
tile += newInfo.stride - width / 8;
tileBase += newInfo.stride - width / 8;
tile += newInfo.stride - newInfo.width;
tileBase += newInfo.stride - newInfo.width;
}
m_ui.x->setText(QString::number(GBAObjAttributesBGetX(obj->b)));
m_ui.y->setText(QString::number(GBAObjAttributesAGetY(obj->a)));
m_ui.w->setText(QString::number(width));
m_ui.h->setText(QString::number(height));
m_ui.x->setText(QString::number(newInfo.x));
m_ui.y->setText(QString::number(newInfo.y));
m_ui.w->setText(QString::number(newInfo.width * 8));
m_ui.h->setText(QString::number(newInfo.height * 8));
m_ui.address->setText(tr("0x%0").arg(BASE_OAM + m_objId * sizeof(*obj), 8, 16, QChar('0')));
m_ui.priority->setText(QString::number(GBAObjAttributesCGetPriority(obj->c)));
m_ui.flippedH->setChecked(GBAObjAttributesBIsHFlip(obj->b));
m_ui.flippedV->setChecked(GBAObjAttributesBIsVFlip(obj->b));
m_ui.enabled->setChecked(!GBAObjAttributesAIsDisable(obj->a) || GBAObjAttributesAIsTransformed(obj->a));
m_ui.priority->setText(QString::number(newInfo.priority));
m_ui.flippedH->setChecked(newInfo.hflip);
m_ui.flippedV->setChecked(newInfo.vflip);
m_ui.enabled->setChecked(newInfo.enabled);
m_ui.doubleSize->setChecked(GBAObjAttributesAIsDoubleSize(obj->a) && GBAObjAttributesAIsTransformed(obj->a));
m_ui.mosaic->setChecked(GBAObjAttributesAIsMosaic(obj->a));
@ -182,39 +153,17 @@ void ObjView::updateTilesGB(bool force) {
const GB* gb = static_cast<const GB*>(m_controller->thread()->core->board);
const GBObj* obj = &gb->video.oam.obj[m_objId];
mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, 0);
unsigned width = 8;
unsigned height = 8;
GBRegisterLCDC lcdc = gb->memory.io[REG_LCDC];
if (GBRegisterLCDCIsObjSize(lcdc)) {
height = 16;
}
unsigned tile = obj->tile;
m_ui.tiles->setTileCount(width * height / 64);
m_ui.tile->setBoundary(1024, 0, 0);
m_ui.tiles->setMinimumSize(QSize(width, height) * m_ui.magnification->value());
m_ui.tiles->resize(QSize(width, height) * m_ui.magnification->value());
unsigned palette = 0;
if (gb->model >= GB_MODEL_CGB) {
if (GBObjAttributesIsBank(obj->attr)) {
tile += 512;
}
palette = GBObjAttributesGetCGBPalette(obj->attr);
} else {
palette = GBObjAttributesGetPalette(obj->attr);
}
m_ui.palette->setText(QString::number(palette));
palette += 8;
ObjInfo newInfo;
lookupObj(m_objId, &newInfo);
mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, 0);
unsigned tile = newInfo.tile;
m_ui.tiles->setTileCount(newInfo.height);
m_ui.tile->setBoundary(1024, 0, 0);
m_ui.tiles->setMinimumSize(QSize(8, newInfo.height * 8) * m_ui.magnification->value());
m_ui.tiles->resize(QSize(8, newInfo.height * 8) * m_ui.magnification->value());
m_ui.palette->setText(QString::number(newInfo.paletteId - 8));
ObjInfo newInfo{
tile,
1,
height / 8,
1,
palette,
0,
2
};
if (newInfo != m_objInfo) {
force = true;
}
@ -223,27 +172,27 @@ void ObjView::updateTilesGB(bool force) {
m_boundary = 1024;
int i = 0;
m_ui.tile->setPalette(palette);
for (int y = 0; y < height / 8; ++y, ++i) {
m_ui.tile->setPalette(newInfo.paletteId);
for (int y = 0; y < newInfo.height; ++y, ++i) {
unsigned t = tile + i;
const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[8 * t], t, palette);
const color_t* data = mTileCacheGetTileIfDirty(tileCache, &m_tileStatus[8 * t], t, newInfo.paletteId);
if (data) {
m_ui.tiles->setTile(i, data);
} else if (force) {
m_ui.tiles->setTile(i, mTileCacheGetTile(tileCache, t, palette));
m_ui.tiles->setTile(i, mTileCacheGetTile(tileCache, t, newInfo.paletteId));
}
}
m_ui.x->setText(QString::number(obj->x));
m_ui.y->setText(QString::number(obj->y));
m_ui.w->setText(QString::number(width));
m_ui.h->setText(QString::number(height));
m_ui.x->setText(QString::number(newInfo.x));
m_ui.y->setText(QString::number(newInfo.y));
m_ui.w->setText(QString::number(8));
m_ui.h->setText(QString::number(newInfo.height * 8));
m_ui.address->setText(tr("0x%0").arg(GB_BASE_OAM + m_objId * sizeof(*obj), 4, 16, QChar('0')));
m_ui.priority->setText(QString::number(GBObjAttributesGetPriority(obj->attr)));
m_ui.flippedH->setChecked(GBObjAttributesIsXFlip(obj->attr));
m_ui.flippedV->setChecked(GBObjAttributesIsYFlip(obj->attr));
m_ui.enabled->setChecked(obj->y != 0 && obj->y < 160);
m_ui.priority->setText(QString::number(newInfo.priority));
m_ui.flippedH->setChecked(newInfo.hflip);
m_ui.flippedV->setChecked(newInfo.vflip);
m_ui.enabled->setChecked(newInfo.enabled);
m_ui.doubleSize->setChecked(false);
m_ui.mosaic->setChecked(false);
m_ui.transform->setText(tr("N/A"));
@ -251,51 +200,10 @@ void ObjView::updateTilesGB(bool force) {
}
#endif
#ifdef USE_PNG
void ObjView::exportObj() {
QString filename = GBAApp::app()->getSaveFileName(this, tr("Export sprite"),
tr("Portable Network Graphics (*.png)"));
VFile* vf = VFileDevice::open(filename, O_WRONLY | O_CREAT | O_TRUNC);
if (!vf) {
LOG(QT, ERROR) << tr("Failed to open output PNG file: %1").arg(filename);
return;
}
CoreController::Interrupter interrupter(m_controller);
png_structp png = PNGWriteOpen(vf);
png_infop info = PNGWriteHeader8(png, m_objInfo.width * 8, m_objInfo.height * 8);
mTileCache* tileCache = mTileCacheSetGetPointer(&m_cacheSet->tiles, m_objInfo.paletteSet);
const color_t* rawPalette = mTileCacheGetPalette(tileCache, m_objInfo.paletteId);
unsigned colors = 1 << m_objInfo.bits;
uint32_t palette[256];
palette[0] = rawPalette[0];
for (unsigned c = 1; c < colors && c < 256; ++c) {
palette[c] = rawPalette[c] | 0xFF000000;
}
PNGWritePalette(png, info, palette, colors);
uint8_t* buffer = new uint8_t[m_objInfo.width * m_objInfo.height * 8 * 8];
unsigned t = m_objInfo.tile;
for (int y = 0; y < m_objInfo.height; ++y) {
for (int x = 0; x < m_objInfo.width; ++x, ++t) {
compositeTile(static_cast<const void*>(mTileCacheGetVRAM(tileCache, t)), reinterpret_cast<color_t*>(buffer), m_objInfo.width * 8, x * 8, y * 8, m_objInfo.bits);
}
t += m_objInfo.stride - m_objInfo.width;
}
PNGWritePixels8(png, m_objInfo.width * 8, m_objInfo.height * 8, m_objInfo.width * 8, static_cast<void*>(buffer));
PNGWriteClose(png, info);
delete[] buffer;
vf->close(vf);
}
#endif
bool ObjView::ObjInfo::operator!=(const ObjInfo& other) {
return other.tile != tile ||
other.width != width ||
other.height != height ||
other.stride != stride ||
other.paletteId != paletteId ||
other.paletteSet != paletteSet;
QImage obj = compositeObj(m_objInfo);
obj.save(filename, "PNG");
}

View File

@ -21,10 +21,8 @@ Q_OBJECT
public:
ObjView(std::shared_ptr<CoreController> controller, QWidget* parent = nullptr);
#ifdef USE_PNG
public slots:
void exportObj();
#endif
private slots:
void selectObj(int);
@ -43,17 +41,7 @@ private:
std::shared_ptr<CoreController> m_controller;
mTileCacheEntry m_tileStatus[1024 * 32] = {}; // TODO: Correct size
int m_objId = 0;
struct ObjInfo {
unsigned tile;
unsigned width;
unsigned height;
unsigned stride;
unsigned paletteId;
unsigned paletteSet;
unsigned bits;
bool operator!=(const ObjInfo&);
} m_objInfo = {};
ObjInfo m_objInfo = {};
int m_tileOffset;
int m_boundary;

View File

@ -30,6 +30,7 @@
#include "DebuggerConsoleController.h"
#include "Display.h"
#include "CoreController.h"
#include "FrameView.h"
#include "GBAApp.h"
#include "GDBController.h"
#include "GDBWindow.h"
@ -1437,7 +1438,7 @@ void Window::setupMenu(QMenuBar* menubar) {
m_overrideView->recheck();
}, "tools");
m_actions.addAction(tr("Game &Pak sensors..."), "sensorWindow", [this]() {
m_actions.addAction(tr("Game Pak sensors..."), "sensorWindow", [this]() {
if (!m_sensorView) {
m_sensorView = std::move(std::make_unique<SensorView>(&m_inputController));
if (m_controller) {
@ -1467,6 +1468,12 @@ void Window::setupMenu(QMenuBar* menubar) {
addGameAction(tr("View &sprites..."), "spriteWindow", openControllerTView<ObjView>(), "tools");
addGameAction(tr("View &tiles..."), "tileWindow", openControllerTView<TileView>(), "tools");
addGameAction(tr("View &map..."), "mapWindow", openControllerTView<MapView>(), "tools");
#ifdef M_CORE_GBA
Action* frameWindow = addGameAction(tr("&Frame inspector..."), "frameWindow", openControllerTView<FrameView>(), "tools");
m_platformActions.insert(PLATFORM_GBA, frameWindow);
#endif
addGameAction(tr("View memory..."), "memoryView", openControllerTView<MemoryView>(), "tools");
addGameAction(tr("Search memory..."), "memorySearch", openControllerTView<MemorySearch>(), "tools");