Qt: Add access log view to memory view

This commit is contained in:
Vicki Pfau 2025-01-12 06:00:40 -08:00
parent 75dc290853
commit 159b0dc445
8 changed files with 473 additions and 11 deletions

View File

@ -262,6 +262,7 @@ if(ENABLE_DEBUGGERS)
DebuggerConsole.cpp
DebuggerConsoleController.cpp
MemoryAccessLogController.cpp
MemoryAccessLogModel.cpp
MemoryAccessLogView.cpp)
endif()

View File

@ -10,8 +10,14 @@
#include "utils.h"
#include "VFileDevice.h"
#include <mgba-util/math.h>
using namespace QGBA;
int MemoryAccessLogController::Flags::count() const {
return popcount32(flags) + popcount32(flagsEx);
}
MemoryAccessLogController::MemoryAccessLogController(CoreController* controller, QObject* parent)
: QObject(parent)
, m_controller(controller)
@ -39,6 +45,17 @@ bool MemoryAccessLogController::canExport() const {
return m_regionMapping.contains("cart0");
}
MemoryAccessLogController::Flags MemoryAccessLogController::flagsForAddress(uint32_t addresss, int segment) {
uint32_t offset = cacheRegion(addresss, segment);
if (!m_cachedRegion) {
return { 0, 0 };
}
return {
m_cachedRegion->blockEx ? m_cachedRegion->blockEx[offset] : mDebuggerAccessLogFlagsEx{},
m_cachedRegion->block ? m_cachedRegion->block[offset] : mDebuggerAccessLogFlags{},
};
}
void MemoryAccessLogController::updateRegion(const QString& internalName, bool checked) {
if (checked) {
m_watchedRegions += internalName;
@ -116,3 +133,27 @@ void MemoryAccessLogController::exportFile(const QString& filename) {
mDebuggerAccessLoggerCreateShadowFile(&m_logger, m_regionMapping[QString("cart0")], vf, 0);
vf->close(vf);
}
uint32_t MemoryAccessLogController::cacheRegion(uint32_t address, int segment) {
if (m_cachedRegion && (address < m_cachedRegion->start || address >= m_cachedRegion->end)) {
m_cachedRegion = nullptr;
}
if (!m_cachedRegion) {
m_cachedRegion = mDebuggerAccessLoggerGetRegion(&m_logger, address, segment, nullptr);
}
if (!m_cachedRegion) {
return 0;
}
size_t offset = address - m_cachedRegion->start;
if (segment > 0) {
uint32_t segmentSize = m_cachedRegion->end - m_cachedRegion->segmentStart;
offset %= segmentSize;
offset += segmentSize * segment;
}
if (offset >= m_cachedRegion->size) {
m_cachedRegion = nullptr;
return cacheRegion(address, segment);
}
return offset;
}

View File

@ -12,6 +12,7 @@
#include "CoreController.h"
#include <mgba/debugger/debugger.h>
#include <mgba/internal/debugger/access-logger.h>
namespace QGBA {
@ -25,6 +26,16 @@ public:
QString internalName;
};
struct Flags {
mDebuggerAccessLogFlagsEx flagsEx;
mDebuggerAccessLogFlags flags;
int count() const;
bool operator==(const Flags& other) const { return flags == other.flags && flagsEx == other.flagsEx; }
bool operator!=(const Flags& other) const { return flags != other.flags || flagsEx != other.flagsEx; }
operator bool() const { return flags || flagsEx; }
};
MemoryAccessLogController(CoreController* controller, QObject* parent = nullptr);
~MemoryAccessLogController();
@ -34,6 +45,8 @@ public:
bool canExport() const;
mPlatform platform() const { return m_controller->platform(); }
Flags flagsForAddress(uint32_t address, int segment = -1);
QString file() const { return m_path; }
bool active() const { return m_active; }
@ -59,8 +72,10 @@ private:
QVector<Region> m_regions;
struct mDebuggerAccessLogger m_logger{};
bool m_active = false;
mDebuggerAccessLogRegion* m_cachedRegion = nullptr;
mDebuggerAccessLogRegionFlags activeFlags() const;
uint32_t cacheRegion(uint32_t address, int segment);
};
}

View File

@ -0,0 +1,283 @@
/* Copyright (c) 2013-2025 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 "MemoryAccessLogModel.h"
#include <limits>
using namespace QGBA;
MemoryAccessLogModel::MemoryAccessLogModel(std::weak_ptr<MemoryAccessLogController> controller, mPlatform platform)
: m_controller(controller)
, m_platform(platform)
{
}
QVariant MemoryAccessLogModel::data(const QModelIndex& index, int role) const {
if (role != Qt::DisplayRole) {
return {};
}
if (index.column() != 0) {
return {};
}
int blockIndex = -1;
int flagIndex = -1;
QModelIndex parent = index.parent();
if (!parent.isValid()) {
blockIndex = index.row();
} else {
blockIndex = parent.row();
flagIndex = index.row();
}
if (blockIndex < 0 || blockIndex >= m_cachedBlocks.count()) {
return {};
}
const Block& block = m_cachedBlocks[blockIndex];
if (flagIndex < 0) {
return QString("0x%1 0x%2")
.arg(QString("%0").arg(block.region.first, 8, 16, QChar('0')).toUpper())
.arg(QString("%0").arg(block.region.second, 8, 16, QChar('0')).toUpper());
}
for (int i = 0; i < 8; ++i) {
if (!(block.flags.flags & (1 << i))) {
continue;
}
if (flagIndex == 0) {
switch (i) {
case 0:
return tr("Data read");
case 1:
return tr("Data written");
case 2:
return tr("Code executed");
case 3:
return tr("Code aborted");
case 4:
return tr("8-bit access");
case 5:
return tr("16-bit access");
case 6:
return tr("32-bit access");
case 7:
return tr("64-bit access");
default:
Q_UNREACHABLE();
}
}
--flagIndex;
}
for (int i = 0; i < 16; ++i) {
if (!(block.flags.flagsEx & (1 << i))) {
continue;
}
if (flagIndex == 0) {
switch (i) {
case 0:
return tr("Accessed by instruction");
case 1:
return tr("Accessed by DMA");
case 2:
return tr("Accessed by BIOS");
case 3:
return tr("Compressed data");
case 4:
return tr("Accessed by memory copy");
case 5:
return tr("(Unknown extra bit 5)");
case 6:
return tr("(Unknown extra bit 6)");
case 7:
return tr("(Unknown extra bit 7)");
case 8:
return tr("Invalid instruction");
case 9:
return tr("Invalid read");
case 10:
return tr("Invalid write");
case 11:
return tr("Invalid executable address");
case 12:
return tr("(Private bit 0)");
case 13:
return tr("(Private bit 1)");
case 14:
switch (m_platform) {
case mPLATFORM_GBA:
return tr("ARM code");
default:
return tr("(Private bit 2)");
}
case 15:
switch (m_platform) {
case mPLATFORM_GBA:
return tr("Thumb code");
default:
return tr("(Private bit 2)");
}
default:
Q_UNREACHABLE();
}
}
--flagIndex;
}
return tr("(Unknown)");
}
QModelIndex MemoryAccessLogModel::index(int row, int column, const QModelIndex& parent) const {
if (column != 0) {
return {};
}
if (parent.isValid()) {
return createIndex(row, 0, parent.row());
}
return createIndex(row, 0, std::numeric_limits<quintptr>::max());
}
QModelIndex MemoryAccessLogModel::parent(const QModelIndex& index) const {
if (!index.isValid()) {
return {};
}
quintptr row = index.internalId();
if (row >= std::numeric_limits<uint32_t>::max()) {
return {};
}
return createIndex(row, 0, std::numeric_limits<quintptr>::max());
}
int MemoryAccessLogModel::rowCount(const QModelIndex& parent) const {
int blockIndex = -1;
if (!parent.isValid()) {
return m_cachedBlocks.count();
} else if (parent.column() != 0) {
return 0;
} else if (parent.parent().isValid()) {
return 0;
} else {
blockIndex = parent.row();
}
if (blockIndex < 0 || blockIndex >= m_cachedBlocks.count()) {
return 0;
}
const Block& block = m_cachedBlocks[blockIndex];
return block.flags.count();
}
void MemoryAccessLogModel::updateSelection(uint32_t start, uint32_t end) {
std::shared_ptr<MemoryAccessLogController> controller = m_controller.lock();
if (!controller) {
return;
}
QVector<Block> newBlocks;
uint32_t lastStart = start;
auto lastFlags = controller->flagsForAddress(m_base + start, m_segment);
for (uint32_t address = start; address < end; ++address) {
auto flags = controller->flagsForAddress(m_base + address, m_segment);
if (flags == lastFlags) {
continue;
}
if (lastFlags) {
newBlocks.append({ lastFlags, qMakePair(lastStart, address) });
}
lastFlags = flags;
lastStart = address;
}
if (lastFlags) {
newBlocks.append({ lastFlags, qMakePair(lastStart, end) });
}
if (m_cachedBlocks.count() == 0 || newBlocks.count() == 0) {
beginResetModel();
m_cachedBlocks = newBlocks;
endResetModel();
return;
}
QPair<int, int> changed{ -1, -1 };
for (int i = 0; i < m_cachedBlocks.count() && i < newBlocks.count(); ++i) {
const Block& oldBlock = m_cachedBlocks.at(i);
const Block& newBlock = newBlocks.at(i);
if (oldBlock != newBlock) {
changed = qMakePair(i, m_cachedBlocks.count());
break;
}
}
if (m_cachedBlocks.count() > newBlocks.count()) {
beginRemoveRows({}, newBlocks.count(), m_cachedBlocks.count());
m_cachedBlocks.resize(newBlocks.count());
endRemoveRows();
changed.second = newBlocks.count();
}
if (m_cachedBlocks.count() < newBlocks.count()) {
beginInsertRows({}, m_cachedBlocks.count(), newBlocks.count());
if (changed.first < 0) {
// Only new rows
m_cachedBlocks = newBlocks;
endInsertRows();
return;
}
}
if (changed.first < 0) {
// No changed rows, though some might have been removed
return;
}
for (int i = 0; i < m_cachedBlocks.count() && i < newBlocks.count(); ++i) {
const Block& oldBlock = m_cachedBlocks.at(i);
const Block& newBlock = newBlocks.at(i);
if (oldBlock.flags != newBlock.flags) {
int oldFlags = oldBlock.flags.count();
int newFlags = newBlock.flags.count();
if (oldFlags > newFlags) {
beginRemoveRows(createIndex(i, 0, std::numeric_limits<quintptr>::max()), newFlags, oldFlags);
} else if (oldFlags < newFlags) {
beginInsertRows(createIndex(i, 0, std::numeric_limits<quintptr>::max()), oldFlags, newFlags);
}
m_cachedBlocks[i] = newBlock;
emit dataChanged(createIndex(0, 0, i), createIndex(std::min(oldFlags, newFlags), 0, i));
if (oldFlags > newFlags) {
endRemoveRows();
} else if (oldFlags < newFlags) {
endInsertRows();
}
}
}
emit dataChanged(createIndex(changed.first, 0, std::numeric_limits<quintptr>::max()),
createIndex(changed.second, 0, std::numeric_limits<quintptr>::max()));
if (m_cachedBlocks.count() < newBlocks.count()) {
m_cachedBlocks = newBlocks;
endInsertRows();
}
}
void MemoryAccessLogModel::setSegment(int segment) {
if (m_segment == segment) {
return;
}
beginResetModel();
m_segment = segment;
m_cachedBlocks.clear();
endResetModel();
}
void MemoryAccessLogModel::setRegion(uint32_t base, uint32_t, bool useSegments) {
if (m_base == base) {
return;
}
beginResetModel();
m_segment = useSegments ? 0 : -1;
m_cachedBlocks.clear();
endResetModel();
}

View File

@ -0,0 +1,55 @@
/* Copyright (c) 2013-2025 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 <QAbstractItemModel>
#include <QVector>
#include "MemoryAccessLogController.h"
struct mCheatDevice;
struct mCheatSet;
namespace QGBA {
class MemoryAccessLogModel : public QAbstractItemModel {
Q_OBJECT
public:
MemoryAccessLogModel(std::weak_ptr<MemoryAccessLogController> controller, mPlatform platform);
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
virtual QModelIndex index(int row, int column, const QModelIndex& parent) const override;
virtual QModelIndex parent(const QModelIndex& index) const override;
virtual int columnCount(const QModelIndex& = QModelIndex()) const override { return 1; }
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
public slots:
void updateSelection(uint32_t start, uint32_t end);
void setSegment(int segment);
void setRegion(uint32_t base, uint32_t segmentSize, bool useSegments);
private:
struct Block {
MemoryAccessLogController::Flags flags;
QPair<uint32_t, uint32_t> region;
bool operator==(const Block& other) const { return flags == other.flags && region == other.region; }
bool operator!=(const Block& other) const { return flags != other.flags || region != other.region; }
};
int flagCount(int index) const;
std::weak_ptr<MemoryAccessLogController> m_controller;
mPlatform m_platform;
uint32_t m_base = 0;
int m_segment = -1;
QVector<Block> m_cachedBlocks;
};
}

View File

@ -7,6 +7,7 @@
#include "MemoryView.h"
#include "CoreController.h"
#include "MemoryAccessLogView.h"
#include "MemoryDump.h"
#include <mgba/core/core.h>
@ -107,6 +108,9 @@ QValidator::State IntValidator::validate(QString& input, int&) const {
MemoryView::MemoryView(std::shared_ptr<CoreController> controller, QWidget* parent)
: QWidget(parent)
, m_controller(controller)
#ifdef ENABLE_DEBUGGERS
, m_malModel(controller->memoryAccessLogController(), controller->platform())
#endif
{
m_ui.setupUi(this);
@ -189,6 +193,22 @@ MemoryView::MemoryView(std::shared_ptr<CoreController> controller, QWidget* pare
}
update();
});
#ifdef ENABLE_DEBUGGERS
connect(m_ui.hexfield, &MemoryModel::selectionChanged, &m_malModel, &MemoryAccessLogModel::updateSelection);
connect(m_ui.segments, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
&m_malModel, &MemoryAccessLogModel::setSegment);
connect(m_ui.accessLoggerButton, &QAbstractButton::clicked, this, [this]() {
std::weak_ptr<MemoryAccessLogController> controller = m_controller->memoryAccessLogController();
MemoryAccessLogView* view = new MemoryAccessLogView(controller);
connect(m_controller.get(), &CoreController::stopping, view, &QWidget::close);
view->setAttribute(Qt::WA_DeleteOnClose);
view->show();
});
m_ui.accessLog->setModel(&m_malModel);
#else
m_ui.accessLog->hide();
#endif
}
void MemoryView::setIndex(int index) {
@ -206,6 +226,10 @@ void MemoryView::setIndex(int index) {
m_ui.segmentColon->setVisible(info.maxSegment > 0);
m_ui.segments->setMaximum(info.maxSegment);
m_ui.hexfield->setRegion(info.start, info.end - info.start, info.shortName);
#ifdef ENABLE_DEBUGGERS
m_malModel.setRegion(info.start, info.segmentStart - info.start, info.maxSegment > 0);
#endif
}
void MemoryView::setSegment(int segment) {

View File

@ -8,6 +8,7 @@
#include <QValidator>
#include "MemoryModel.h"
#include "MemoryAccessLogModel.h"
#include "ui_MemoryView.h"
@ -54,6 +55,10 @@ private:
std::shared_ptr<CoreController> m_controller;
QPair<uint32_t, uint32_t> m_region;
QPair<uint32_t, uint32_t> m_selection;
#ifdef ENABLE_DEBUGGERS
MemoryAccessLogModel m_malModel;
#endif
};
}

View File

@ -31,14 +31,14 @@
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="sidebarLayout" stretch="0,0,0,1,0,1">
<item>
<widget class="QComboBox" name="regions"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QHBoxLayout" name="addressLayout">
<item>
<widget class="QLabel" name="label">
<widget class="QLabel" name="address">
<property name="text">
<string>Address:</string>
</property>
@ -105,9 +105,9 @@
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<layout class="QHBoxLayout" name="alignmentLayout">
<item>
<widget class="QLabel" name="label_2">
<widget class="QLabel" name="alignmentLabel">
<property name="text">
<string>Alignment:</string>
</property>
@ -141,7 +141,7 @@
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<widget class="QGroupBox" name="data">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
@ -151,9 +151,9 @@
<property name="title">
<string/>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<layout class="QGridLayout" name="gridLayout_9">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<widget class="QLabel" name="sintLabel">
<property name="text">
<string>Signed:</string>
</property>
@ -173,7 +173,7 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<widget class="QLabel" name="uintLabel">
<property name="text">
<string>Unsigned:</string>
</property>
@ -193,7 +193,7 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<widget class="QLabel" name="stringLabel">
<property name="text">
<string>String:</string>
</property>
@ -232,7 +232,7 @@
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<layout class="QGridLayout" name="buttons">
<item row="0" column="0">
<widget class="QPushButton" name="copy">
<property name="text">
@ -276,6 +276,44 @@
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="verticalGroupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Selected address accesses</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="accessLog">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textElideMode">
<enum>Qt::ElideNone</enum>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QPushButton" name="accessLoggerButton">
<property name="text">
<string>Logging configuration</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>