diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 910ab9b57..9858fddb0 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -262,6 +262,7 @@ if(ENABLE_DEBUGGERS) DebuggerConsole.cpp DebuggerConsoleController.cpp MemoryAccessLogController.cpp + MemoryAccessLogModel.cpp MemoryAccessLogView.cpp) endif() diff --git a/src/platform/qt/MemoryAccessLogController.cpp b/src/platform/qt/MemoryAccessLogController.cpp index 95200e53e..9691dcadf 100644 --- a/src/platform/qt/MemoryAccessLogController.cpp +++ b/src/platform/qt/MemoryAccessLogController.cpp @@ -10,8 +10,14 @@ #include "utils.h" #include "VFileDevice.h" +#include + 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; +} diff --git a/src/platform/qt/MemoryAccessLogController.h b/src/platform/qt/MemoryAccessLogController.h index 42ef655c3..79553b333 100644 --- a/src/platform/qt/MemoryAccessLogController.h +++ b/src/platform/qt/MemoryAccessLogController.h @@ -12,6 +12,7 @@ #include "CoreController.h" +#include #include 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 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); }; } diff --git a/src/platform/qt/MemoryAccessLogModel.cpp b/src/platform/qt/MemoryAccessLogModel.cpp new file mode 100644 index 000000000..23eb34f4c --- /dev/null +++ b/src/platform/qt/MemoryAccessLogModel.cpp @@ -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 + +using namespace QGBA; + +MemoryAccessLogModel::MemoryAccessLogModel(std::weak_ptr 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::max()); +} + +QModelIndex MemoryAccessLogModel::parent(const QModelIndex& index) const { + if (!index.isValid()) { + return {}; + } + quintptr row = index.internalId(); + if (row >= std::numeric_limits::max()) { + return {}; + } + return createIndex(row, 0, std::numeric_limits::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 controller = m_controller.lock(); + if (!controller) { + return; + } + QVector 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 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::max()), newFlags, oldFlags); + } else if (oldFlags < newFlags) { + beginInsertRows(createIndex(i, 0, std::numeric_limits::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::max()), + createIndex(changed.second, 0, std::numeric_limits::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(); +} diff --git a/src/platform/qt/MemoryAccessLogModel.h b/src/platform/qt/MemoryAccessLogModel.h new file mode 100644 index 000000000..b18f0dee5 --- /dev/null +++ b/src/platform/qt/MemoryAccessLogModel.h @@ -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 +#include + +#include "MemoryAccessLogController.h" + +struct mCheatDevice; +struct mCheatSet; + +namespace QGBA { + +class MemoryAccessLogModel : public QAbstractItemModel { +Q_OBJECT + +public: + MemoryAccessLogModel(std::weak_ptr 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 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 m_controller; + mPlatform m_platform; + uint32_t m_base = 0; + int m_segment = -1; + QVector m_cachedBlocks; +}; + +} diff --git a/src/platform/qt/MemoryView.cpp b/src/platform/qt/MemoryView.cpp index cdb754bfe..009d789aa 100644 --- a/src/platform/qt/MemoryView.cpp +++ b/src/platform/qt/MemoryView.cpp @@ -7,6 +7,7 @@ #include "MemoryView.h" #include "CoreController.h" +#include "MemoryAccessLogView.h" #include "MemoryDump.h" #include @@ -107,6 +108,9 @@ QValidator::State IntValidator::validate(QString& input, int&) const { MemoryView::MemoryView(std::shared_ptr 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 controller, QWidget* pare } update(); }); + +#ifdef ENABLE_DEBUGGERS + connect(m_ui.hexfield, &MemoryModel::selectionChanged, &m_malModel, &MemoryAccessLogModel::updateSelection); + connect(m_ui.segments, static_cast(&QSpinBox::valueChanged), + &m_malModel, &MemoryAccessLogModel::setSegment); + connect(m_ui.accessLoggerButton, &QAbstractButton::clicked, this, [this]() { + std::weak_ptr 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) { diff --git a/src/platform/qt/MemoryView.h b/src/platform/qt/MemoryView.h index f68052a06..d6a5c6f93 100644 --- a/src/platform/qt/MemoryView.h +++ b/src/platform/qt/MemoryView.h @@ -8,6 +8,7 @@ #include #include "MemoryModel.h" +#include "MemoryAccessLogModel.h" #include "ui_MemoryView.h" @@ -54,6 +55,10 @@ private: std::shared_ptr m_controller; QPair m_region; QPair m_selection; + +#ifdef ENABLE_DEBUGGERS + MemoryAccessLogModel m_malModel; +#endif }; } diff --git a/src/platform/qt/MemoryView.ui b/src/platform/qt/MemoryView.ui index 06ab570d1..73407fdd1 100644 --- a/src/platform/qt/MemoryView.ui +++ b/src/platform/qt/MemoryView.ui @@ -31,14 +31,14 @@ - + - + - + Address: @@ -105,9 +105,9 @@ - + - + Alignment: @@ -141,7 +141,7 @@ - + 0 @@ -151,9 +151,9 @@ - + - + Signed: @@ -173,7 +173,7 @@ - + Unsigned: @@ -193,7 +193,7 @@ - + String: @@ -232,7 +232,7 @@ - + @@ -276,6 +276,44 @@ + + + + + 0 + 0 + + + + Selected address accesses + + + + + + + 0 + 0 + + + + Qt::ElideNone + + + false + + + + + + + Logging configuration + + + + + +