diff --git a/CHANGES b/CHANGES index b6a6143fe..8b449be32 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/src/platform/qt/AssetView.cpp b/src/platform/qt/AssetView.cpp index 2f48cce32..eef257025 100644 --- a/src/platform/qt/AssetView.cpp +++ b/src/platform/qt/AssetView.cpp @@ -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 +#ifdef M_CORE_GBA +#include +#endif +#ifdef M_CORE_GB +#include +#include +#endif + +#include + using namespace QGBA; AssetView::AssetView(std::shared_ptr 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(&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 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(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(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(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; +} \ No newline at end of file diff --git a/src/platform/qt/AssetView.h b/src/platform/qt/AssetView.h index a95483c5e..779e22890 100644 --- a/src/platform/qt/AssetView.h +++ b/src/platform/qt/AssetView.h @@ -12,6 +12,8 @@ #include +struct mMapCacheEntry; + namespace QGBA { class CoreController; @@ -22,8 +24,6 @@ Q_OBJECT public: AssetView(std::shared_ptr 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 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 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; }; diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 0dbb75efa..49be1cda7 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -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 diff --git a/src/platform/qt/FrameView.cpp b/src/platform/qt/FrameView.cpp new file mode 100644 index 000000000..121bd9171 --- /dev/null +++ b/src/platform/qt/FrameView.cpp @@ -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 +#include +#include + +#include + +#include "CoreController.h" + +#ifdef M_CORE_GBA +#include +#include +#include +#include +#endif +#ifdef M_CORE_GB +#include +#include +#endif + +using namespace QGBA; + +FrameView::FrameView(std::shared_ptr 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(m_controller->thread()->core->board)->memory.io; + int mode = GBARegisterDISPCNTGetMode(io[REG_DISPCNT >> 1]); + + std::array 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(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); +} \ No newline at end of file diff --git a/src/platform/qt/FrameView.h b/src/platform/qt/FrameView.h new file mode 100644 index 000000000..626dfbd62 --- /dev/null +++ b/src/platform/qt/FrameView.h @@ -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 +#include +#include +#include +#include +#include + +#include "AssetView.h" + +#include + +namespace QGBA { + +class CoreController; + +class FrameView : public AssetView { +Q_OBJECT + +public: + FrameView(std::shared_ptr 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 m_queue; + QSet m_disabled; + QPixmap m_composited; + QPixmap m_rendered; + mMapCacheEntry m_mapStatus[4][128 * 128] = {}; // TODO: Correct size +}; + +} \ No newline at end of file diff --git a/src/platform/qt/FrameView.ui b/src/platform/qt/FrameView.ui new file mode 100644 index 000000000..2e61e09c8 --- /dev/null +++ b/src/platform/qt/FrameView.ui @@ -0,0 +1,156 @@ + + + FrameView + + + + 0 + 0 + 869 + 875 + + + + Inspect frame + + + + + + + + + 0 + 0 + + + + × + + + 1 + + + 8 + + + + + + + Magnification + + + + + + + + + Freeze frame + + + + + + + true + + + + + 0 + 0 + 591 + 403 + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + true + + + + + 0 + 0 + 591 + 446 + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + false + + + Export + + + + + + + + 0 + 0 + + + + + + + + + diff --git a/src/platform/qt/MapView.cpp b/src/platform/qt/MapView.cpp index 91a283121..0fe80c2c3 100644 --- a/src/platform/qt/MapView.cpp +++ b/src/platform/qt/MapView.cpp @@ -211,6 +211,7 @@ void MapView::updateTilesGBA(bool force) { mBitmapCacheCleanRow(bitmapCache, m_bitmapStatus, j); memcpy(static_cast(&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(&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()); diff --git a/src/platform/qt/MapView.ui b/src/platform/qt/MapView.ui index b9d5418f3..d21cce841 100644 --- a/src/platform/qt/MapView.ui +++ b/src/platform/qt/MapView.ui @@ -6,8 +6,8 @@ 0 0 - 1273 - 736 + 941 + 617 @@ -92,8 +92,8 @@ 0 0 - 835 - 720 + 613 + 601 diff --git a/src/platform/qt/ObjView.cpp b/src/platform/qt/ObjView.cpp index 64b689640..e482f023b 100644 --- a/src/platform/qt/ObjView.cpp +++ b/src/platform/qt/ObjView.cpp @@ -19,9 +19,7 @@ #endif #ifdef M_CORE_GB #include -#include #endif -#include #include using namespace QGBA; @@ -53,11 +51,7 @@ ObjView::ObjView(std::shared_ptr controller, QWidget* parent) connect(m_ui.magnification, static_cast(&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(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(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(mTileCacheGetVRAM(tileCache, t)), reinterpret_cast(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(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"); } diff --git a/src/platform/qt/ObjView.h b/src/platform/qt/ObjView.h index 41677ca92..42cd3f65e 100644 --- a/src/platform/qt/ObjView.h +++ b/src/platform/qt/ObjView.h @@ -21,10 +21,8 @@ Q_OBJECT public: ObjView(std::shared_ptr 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 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; diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index d87674d55..fc4d9a6e2 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -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(&m_inputController)); if (m_controller) { @@ -1467,6 +1468,12 @@ void Window::setupMenu(QMenuBar* menubar) { addGameAction(tr("View &sprites..."), "spriteWindow", openControllerTView(), "tools"); addGameAction(tr("View &tiles..."), "tileWindow", openControllerTView(), "tools"); addGameAction(tr("View &map..."), "mapWindow", openControllerTView(), "tools"); + +#ifdef M_CORE_GBA + Action* frameWindow = addGameAction(tr("&Frame inspector..."), "frameWindow", openControllerTView(), "tools"); + m_platformActions.insert(PLATFORM_GBA, frameWindow); +#endif + addGameAction(tr("View memory..."), "memoryView", openControllerTView(), "tools"); addGameAction(tr("Search memory..."), "memorySearch", openControllerTView(), "tools");