Qt: Add frame inspector for GBA games
jump to
@@ -11,6 +11,7 @@ - Enhanced map viewer, supporting bitmapped GBA modes and more displayed info
- 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
@@ -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@@ -8,6 +8,16 @@
#include "CoreController.h" #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;@@ -98,3 +108,166 @@ }
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; +}
@@ -12,6 +12,8 @@ #include <mgba/core/cache-set.h>
#include <memory> +struct mMapCacheEntry; + namespace QGBA { class CoreController;@@ -21,8 +23,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();@@ -40,9 +40,42 @@ void resizeEvent(QResizeEvent*) override;
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; };
@@ -72,6 +72,7 @@ CoreController.cpp
Display.cpp DisplayGL.cpp DisplayQt.cpp + FrameView.cpp GBAApp.cpp GBAKeyEditor.cpp GIFView.cpp@@ -122,6 +123,7 @@ AssetTile.ui
BattleChipView.ui CheatsView.ui DebuggerConsole.ui + FrameView.ui GIFView.ui IOViewer.ui LoadSaveState.ui
@@ -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); +}
@@ -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 +}; + +}
@@ -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>
@@ -211,6 +211,7 @@ for (int j = 0; j < height; ++j) {
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 @@ m_ui.bgInfo->setCustomProperty("size", QString("%1×%2").arg(tilesW * 8).arg(tilesH * 8));
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());
@@ -6,8 +6,8 @@ <property name="geometry">
<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 @@ <property name="geometry">
<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">
@@ -19,9 +19,7 @@ #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-util/png-io.h> #include <mgba-util/vfs.h> using namespace QGBA;@@ -53,11 +51,7 @@ connect(m_ui.objId, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &ObjView::selectObj);
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 @@ m_ui.objId->setMaximum(127);
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 @@ m_ui.objId->setMaximum(39);
const GB* gb = static_cast<const GB*>(m_controller->thread()->core->board); const GBObj* obj = &gb->video.oam.obj[m_objId]; + ObjInfo newInfo; + lookupObj(m_objId, &newInfo); + 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); + unsigned tile = newInfo.tile; + m_ui.tiles->setTileCount(newInfo.height); 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; + 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 @@ m_tileOffset = tile;
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 @@ m_ui.mode->setText(tr("N/A"));
} #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"); }
@@ -21,10 +21,8 @@
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 @@
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;
@@ -30,6 +30,7 @@ #include "DebuggerConsole.h"
#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 @@ m_overrideView->show();
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 @@ addGameAction(tr("View &palette..."), "paletteWindow", openControllerTView<PaletteView>(), "tools");
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");