mirror of
https://github.com/stenzek/duckstation.git
synced 2026-02-09 07:44:32 +00:00
Qt: Add Memory Editor window
This commit is contained in:
@@ -3195,7 +3195,7 @@ bool CPU::SafeReadMemoryWord(VirtualMemoryAddress addr, u32* value)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CPU::SafeReadMemoryCString(VirtualMemoryAddress addr, std::string* value, u32 max_length /*= 1024*/)
|
||||
bool CPU::SafeReadMemoryCString(VirtualMemoryAddress addr, SmallStringBase* value, u32 max_length /*= 1024*/)
|
||||
{
|
||||
value->clear();
|
||||
|
||||
@@ -3206,7 +3206,7 @@ bool CPU::SafeReadMemoryCString(VirtualMemoryAddress addr, std::string* value, u
|
||||
return true;
|
||||
|
||||
value->push_back(ch);
|
||||
if (value->size() >= max_length)
|
||||
if (value->length() >= max_length)
|
||||
return true;
|
||||
|
||||
addr++;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <vector>
|
||||
|
||||
class StateWrapper;
|
||||
class SmallStringBase;
|
||||
|
||||
namespace CPU {
|
||||
|
||||
@@ -182,7 +183,7 @@ ALWAYS_INLINE bool InKernelMode()
|
||||
bool SafeReadMemoryByte(VirtualMemoryAddress addr, u8* value);
|
||||
bool SafeReadMemoryHalfWord(VirtualMemoryAddress addr, u16* value);
|
||||
bool SafeReadMemoryWord(VirtualMemoryAddress addr, u32* value);
|
||||
bool SafeReadMemoryCString(VirtualMemoryAddress addr, std::string* value, u32 max_length = 1024);
|
||||
bool SafeReadMemoryCString(VirtualMemoryAddress addr, SmallStringBase* value, u32 max_length = 1024);
|
||||
bool SafeReadMemoryBytes(VirtualMemoryAddress addr, void* data, u32 length);
|
||||
bool SafeWriteMemoryByte(VirtualMemoryAddress addr, u8 value);
|
||||
bool SafeWriteMemoryHalfWord(VirtualMemoryAddress addr, u16 value);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/small_string.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
LOG_CHANNEL(PCDrv);
|
||||
@@ -77,7 +78,7 @@ static bool CloseFileHandle(u32 handle)
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string ResolveHostPath(const std::string& path)
|
||||
static std::string ResolveHostPath(std::string_view path)
|
||||
{
|
||||
// Double-check that it falls within the directory of the root.
|
||||
// Not a real sandbox, but emulators shouldn't be treated as such. Don't run untrusted code!
|
||||
@@ -142,7 +143,7 @@ bool PCDrv::HandleSyscall(u32 instruction_bits, CPU::Registers& regs)
|
||||
const bool is_open = (code == 0x103);
|
||||
const char* func = (code == 0x102) ? "PCcreat" : "PCopen";
|
||||
const u32 mode = regs.a2;
|
||||
std::string filename;
|
||||
SmallString filename;
|
||||
if (!CPU::SafeReadMemoryCString(regs.a1, &filename))
|
||||
{
|
||||
ERROR_LOG("{}: Invalid string", func);
|
||||
|
||||
@@ -120,6 +120,9 @@ set(SRCS
|
||||
memorycardrenamefiledialog.ui
|
||||
memorycardsettingswidget.cpp
|
||||
memorycardsettingswidget.h
|
||||
memoryeditorwindow.cpp
|
||||
memoryeditorwindow.h
|
||||
memoryeditorwindow.ui
|
||||
memoryscannerwindow.cpp
|
||||
memoryscannerwindow.h
|
||||
memoryscannerwindow.ui
|
||||
|
||||
@@ -522,12 +522,6 @@ void DebuggerWindow::connectSignals()
|
||||
m_refresh_timer.setInterval(TIMER_REFRESH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
void DebuggerWindow::disconnectSignals()
|
||||
{
|
||||
EmuThread* hi = g_emu_thread;
|
||||
hi->disconnect(this);
|
||||
}
|
||||
|
||||
void DebuggerWindow::createModels()
|
||||
{
|
||||
m_registers_model = new DebuggerRegistersModel(this);
|
||||
@@ -570,6 +564,8 @@ void DebuggerWindow::setUIEnabled(bool enabled, bool allow_pause)
|
||||
m_ui.memoryRegionEXP1->setEnabled(read_only_views);
|
||||
m_ui.memoryRegionScratchpad->setEnabled(read_only_views);
|
||||
m_ui.memoryRegionBIOS->setEnabled(read_only_views);
|
||||
m_ui.memorySearch->setEnabled(read_only_views);
|
||||
m_ui.memorySearchString->setEnabled(read_only_views);
|
||||
|
||||
// Partial/timer refreshes only active when not paused.
|
||||
const bool timer_active = (!enabled && allow_pause);
|
||||
@@ -597,11 +593,13 @@ void DebuggerWindow::setMemoryViewRegion(Bus::MemoryRegion region)
|
||||
|
||||
const u32 start_page = static_cast<u32>(offset) >> HOST_PAGE_SHIFT;
|
||||
const u32 end_page = static_cast<u32>(offset + count - 1) >> HOST_PAGE_SHIFT;
|
||||
for (u32 i = start_page; i <= end_page; i++)
|
||||
{
|
||||
if (Bus::g_ram_code_bits[i])
|
||||
CPU::CodeCache::InvalidateBlocksWithPageIndex(i);
|
||||
}
|
||||
Host::RunOnCPUThread([start_page, end_page]() {
|
||||
for (u32 i = start_page; i <= end_page; i++)
|
||||
{
|
||||
if (Bus::g_ram_code_bits[i])
|
||||
CPU::CodeCache::InvalidateBlocksWithPageIndex(i);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const PhysicalMemoryAddress start = Bus::GetMemoryRegionStart(region);
|
||||
|
||||
@@ -36,7 +36,6 @@ protected:
|
||||
private:
|
||||
void setupAdditionalUi();
|
||||
void connectSignals();
|
||||
void disconnectSignals();
|
||||
void createModels();
|
||||
void setUIEnabled(bool enabled, bool allow_pause);
|
||||
void saveCurrentState();
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<ClCompile Include="inputbindingwidgets.cpp" />
|
||||
<ClCompile Include="isobrowserwindow.cpp" />
|
||||
<ClCompile Include="logwindow.cpp" />
|
||||
<ClCompile Include="memoryeditorwindow.cpp" />
|
||||
<ClCompile Include="memoryscannerwindow.cpp" />
|
||||
<ClCompile Include="memoryviewwidget.cpp" />
|
||||
<ClCompile Include="displaywidget.cpp" />
|
||||
@@ -79,7 +80,8 @@
|
||||
<QtMoc Include="controllersettingswindow.h" />
|
||||
<QtMoc Include="colorpickerbutton.h" />
|
||||
<ClInclude Include="controllersettingwidgetbinder.h" />
|
||||
<ClInclude Include="memoryviewwidget.h" />
|
||||
<QtMoc Include="memoryeditorwindow.h" />
|
||||
<QtMoc Include="memoryviewwidget.h" />
|
||||
<QtMoc Include="logwindow.h" />
|
||||
<QtMoc Include="graphicssettingswidget.h" />
|
||||
<QtMoc Include="memoryscannerwindow.h" />
|
||||
@@ -315,6 +317,9 @@
|
||||
<QtUi Include="postprocessingoverlayconfigwidget.ui">
|
||||
<FileType>Document</FileType>
|
||||
</QtUi>
|
||||
<QtUi Include="memoryeditorwindow.ui">
|
||||
<FileType>Document</FileType>
|
||||
</QtUi>
|
||||
<None Include="translations\duckstation-qt_es-es.ts" />
|
||||
<None Include="translations\duckstation-qt_sv.ts" />
|
||||
<None Include="translations\duckstation-qt_tr.ts" />
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<ClCompile Include="isobrowserwindow.cpp" />
|
||||
<ClCompile Include="debuggercodeview.cpp" />
|
||||
<ClCompile Include="togglebutton.cpp" />
|
||||
<ClCompile Include="memoryeditorwindow.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="qtutils.h" />
|
||||
@@ -56,7 +57,6 @@
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="controllersettingwidgetbinder.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="memoryviewwidget.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="resources">
|
||||
@@ -110,6 +110,8 @@
|
||||
<QtMoc Include="isobrowserwindow.h" />
|
||||
<QtMoc Include="debuggercodeview.h" />
|
||||
<QtMoc Include="togglebutton.h" />
|
||||
<QtMoc Include="memoryeditorwindow.h" />
|
||||
<QtMoc Include="memoryviewwidget.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUi Include="consolesettingswidget.ui" />
|
||||
@@ -163,6 +165,7 @@
|
||||
<QtUi Include="memorycardrenamefiledialog.ui" />
|
||||
<QtUi Include="isobrowserwindow.ui" />
|
||||
<QtUi Include="postprocessingoverlayconfigwidget.ui" />
|
||||
<QtUi Include="memoryeditorwindow.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="duckstation-qt.rc" />
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "isobrowserwindow.h"
|
||||
#include "logwindow.h"
|
||||
#include "memorycardeditorwindow.h"
|
||||
#include "memoryeditorwindow.h"
|
||||
#include "memoryscannerwindow.h"
|
||||
#include "qthost.h"
|
||||
#include "qtutils.h"
|
||||
@@ -881,6 +882,7 @@ void MainWindow::recreate()
|
||||
void MainWindow::destroySubWindows()
|
||||
{
|
||||
QtUtils::CloseAndDeleteWindow(m_cover_download_window);
|
||||
QtUtils::CloseAndDeleteWindow(m_memory_editor_window);
|
||||
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
|
||||
QtUtils::CloseAndDeleteWindow(m_debugger_window);
|
||||
QtUtils::CloseAndDeleteWindow(m_memory_card_editor_window);
|
||||
@@ -2135,6 +2137,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool achiev
|
||||
m_ui.menuChangeDisc->setDisabled(starting_or_not_running);
|
||||
m_ui.menuCheats->setDisabled(starting_or_not_running || achievements_hardcore_mode);
|
||||
m_ui.actionCPUDebugger->setDisabled(achievements_hardcore_mode);
|
||||
m_ui.actionMemoryEditor->setDisabled(achievements_hardcore_mode);
|
||||
m_ui.actionMemoryScanner->setDisabled(achievements_hardcore_mode);
|
||||
m_ui.actionReloadTextureReplacements->setDisabled(starting_or_not_running);
|
||||
m_ui.actionDumpRAM->setDisabled(starting_or_not_running || achievements_hardcore_mode);
|
||||
@@ -2443,6 +2446,7 @@ void MainWindow::connectSignals()
|
||||
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
|
||||
connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
|
||||
connect(m_ui.actionMemoryCardEditor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered);
|
||||
connect(m_ui.actionMemoryEditor, &QAction::triggered, this, &MainWindow::onToolsMemoryEditorTriggered);
|
||||
connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered);
|
||||
connect(m_ui.actionISOBrowser, &QAction::triggered, this, &MainWindow::onToolsISOBrowserTriggered);
|
||||
connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
|
||||
@@ -2728,6 +2732,20 @@ ControllerSettingsWindow* MainWindow::getControllerSettingsWindow()
|
||||
return m_controller_settings_window;
|
||||
}
|
||||
|
||||
MemoryEditorWindow* MainWindow::getMemoryEditorWindow()
|
||||
{
|
||||
if (!m_memory_editor_window)
|
||||
{
|
||||
m_memory_editor_window = new MemoryEditorWindow();
|
||||
connect(m_memory_editor_window, &MemoryEditorWindow::closed, this, [this]() {
|
||||
m_memory_editor_window->deleteLater();
|
||||
m_memory_editor_window = nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
return m_memory_editor_window;
|
||||
}
|
||||
|
||||
void MainWindow::doControllerSettings(
|
||||
ControllerSettingsWindow::Category category /*= ControllerSettingsDialog::Category::Count*/)
|
||||
{
|
||||
@@ -3147,6 +3165,7 @@ void MainWindow::onAchievementsHardcoreModeChanged(bool enabled)
|
||||
if (enabled)
|
||||
{
|
||||
QtUtils::CloseAndDeleteWindow(m_debugger_window);
|
||||
QtUtils::CloseAndDeleteWindow(m_memory_editor_window);
|
||||
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
|
||||
}
|
||||
|
||||
@@ -3251,6 +3270,14 @@ void MainWindow::onToolsMediaCaptureToggled(bool checked)
|
||||
Host::RunOnCPUThread([path = path.toStdString()]() { System::StartMediaCapture(path); });
|
||||
}
|
||||
|
||||
void MainWindow::onToolsMemoryEditorTriggered()
|
||||
{
|
||||
if (s_achievements_hardcore_mode)
|
||||
return;
|
||||
|
||||
QtUtils::ShowOrRaiseWindow(getMemoryEditorWindow());
|
||||
}
|
||||
|
||||
void MainWindow::onToolsMemoryScannerTriggered()
|
||||
{
|
||||
if (s_achievements_hardcore_mode)
|
||||
|
||||
@@ -33,6 +33,7 @@ class AutoUpdaterWindow;
|
||||
class MemoryCardEditorWindow;
|
||||
class DebuggerWindow;
|
||||
class MemoryScannerWindow;
|
||||
class MemoryEditorWindow;
|
||||
class CoverDownloadWindow;
|
||||
|
||||
struct SystemBootParameters;
|
||||
@@ -114,6 +115,7 @@ public:
|
||||
/// Returns pointer to settings window.
|
||||
SettingsWindow* getSettingsWindow();
|
||||
ControllerSettingsWindow* getControllerSettingsWindow();
|
||||
MemoryEditorWindow* getMemoryEditorWindow();
|
||||
|
||||
/// Updates debug menu visibility (hides if disabled).
|
||||
void updateDebugMenuVisibility();
|
||||
@@ -292,6 +294,7 @@ private:
|
||||
void onAboutActionTriggered();
|
||||
void onCheckForUpdatesActionTriggered();
|
||||
void onToolsMemoryCardEditorTriggered();
|
||||
void onToolsMemoryEditorTriggered();
|
||||
void onToolsMemoryScannerTriggered();
|
||||
void onToolsISOBrowserTriggered();
|
||||
void onToolsCoverDownloaderTriggered();
|
||||
@@ -345,6 +348,7 @@ private:
|
||||
MemoryCardEditorWindow* m_memory_card_editor_window = nullptr;
|
||||
DebuggerWindow* m_debugger_window = nullptr;
|
||||
MemoryScannerWindow* m_memory_scanner_window = nullptr;
|
||||
MemoryEditorWindow* m_memory_editor_window = nullptr;
|
||||
CoverDownloadWindow* m_cover_download_window = nullptr;
|
||||
|
||||
bool m_was_paused_by_focus_loss = false;
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
<addaction name="actionCoverDownloader"/>
|
||||
<addaction name="actionControllerTest"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionMemoryEditor"/>
|
||||
<addaction name="actionMemoryScanner"/>
|
||||
<addaction name="actionISOBrowser"/>
|
||||
<addaction name="separator"/>
|
||||
@@ -669,12 +670,12 @@
|
||||
<property name="text">
|
||||
<string>&Settings</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::PreferencesRole</enum>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Opens the settings window.</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::PreferencesRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSettings2">
|
||||
<property name="icon">
|
||||
@@ -683,12 +684,12 @@
|
||||
<property name="text">
|
||||
<string>&Settings</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::PreferencesRole</enum>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Opens the settings window.</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::PreferencesRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionChangeDiscFromFile">
|
||||
<property name="text">
|
||||
@@ -1292,6 +1293,14 @@
|
||||
<string>Shows titles for games in their native language.</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionMemoryEditor">
|
||||
<property name="text">
|
||||
<string>Memory &Editor</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Opens the memory editor window.</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="resources/duckstation-qt.qrc"/>
|
||||
|
||||
632
src/duckstation-qt/memoryeditorwindow.cpp
Normal file
632
src/duckstation-qt/memoryeditorwindow.cpp
Normal file
@@ -0,0 +1,632 @@
|
||||
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "memoryeditorwindow.h"
|
||||
#include "mainwindow.h"
|
||||
#include "qthost.h"
|
||||
#include "qtutils.h"
|
||||
|
||||
#include "core/bus.h"
|
||||
#include "core/cpu_code_cache.h"
|
||||
#include "core/cpu_core_private.h"
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
#include <QtCore/QSignalBlocker>
|
||||
#include <QtGui/QCursor>
|
||||
#include <QtGui/QFontDatabase>
|
||||
#include <QtGui/QShortcut>
|
||||
#include <QtWidgets/QAbstractScrollArea>
|
||||
#include <QtWidgets/QFileDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <bit>
|
||||
|
||||
#include "moc_memoryeditorwindow.cpp"
|
||||
|
||||
static constexpr int TIMER_REFRESH_INTERVAL_MS = 100;
|
||||
|
||||
MemoryEditorWindow::MemoryEditorWindow(QWidget* parent /* = nullptr */) : QWidget(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
setupAdditionalUi();
|
||||
connectSignals();
|
||||
updateUIEnabled();
|
||||
updateMemoryViewRegion();
|
||||
updateDataInspector();
|
||||
}
|
||||
|
||||
MemoryEditorWindow::~MemoryEditorWindow() = default;
|
||||
|
||||
void MemoryEditorWindow::onSystemStarted()
|
||||
{
|
||||
updateUIEnabled();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onSystemDestroyed()
|
||||
{
|
||||
updateUIEnabled();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onSystemPaused()
|
||||
{
|
||||
updateUIEnabled();
|
||||
refreshAll();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onSystemResumed()
|
||||
{
|
||||
updateUIEnabled();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::timerRefresh()
|
||||
{
|
||||
m_ui.memoryView->forceRefresh();
|
||||
updateDataInspector();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::refreshAll()
|
||||
{
|
||||
m_ui.memoryView->forceRefresh();
|
||||
updateDataInspector();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onMemoryViewTopAddressChanged(size_t address)
|
||||
{
|
||||
m_ui.address->setText(formatAddress(static_cast<VirtualMemoryAddress>(address)));
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onAddressEditingFinished()
|
||||
{
|
||||
QString address_str = m_ui.address->text();
|
||||
if (address_str.startsWith(QStringLiteral("0x")) || address_str.startsWith(QStringLiteral("0X")))
|
||||
address_str = address_str.mid(2);
|
||||
|
||||
const std::optional<VirtualMemoryAddress> address =
|
||||
StringUtil::FromChars<VirtualMemoryAddress>(address_str.toStdString(), 16);
|
||||
if (!address.has_value())
|
||||
{
|
||||
m_ui.address->setText(formatAddress(static_cast<VirtualMemoryAddress>(m_ui.memoryView->topAddress())));
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToMemoryAddress(address.value());
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onDumpAddressTriggered()
|
||||
{
|
||||
std::optional<VirtualMemoryAddress> address =
|
||||
QtUtils::PromptForAddress(this, windowTitle(), tr("Enter memory address:"), false);
|
||||
if (!address.has_value())
|
||||
return;
|
||||
|
||||
scrollToMemoryAddress(address.value());
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onMemoryRegionButtonToggled(QAbstractButton*, bool checked)
|
||||
{
|
||||
if (!checked)
|
||||
return;
|
||||
|
||||
updateMemoryViewRegion();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onDataInspectorBaseButtonToggled(QAbstractButton*, bool checked)
|
||||
{
|
||||
if (!checked)
|
||||
return;
|
||||
|
||||
updateDataInspector();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onDataInspectorEndianButtonToggled(QAbstractButton*, bool checked)
|
||||
{
|
||||
if (!checked)
|
||||
return;
|
||||
|
||||
updateDataInspector();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onMemorySearchTriggered()
|
||||
{
|
||||
m_ui.memoryView->clearHighlightRange();
|
||||
|
||||
const QString pattern_str = m_ui.memorySearchString->text();
|
||||
if (pattern_str.isEmpty())
|
||||
return;
|
||||
|
||||
std::vector<u8> pattern;
|
||||
std::vector<u8> mask;
|
||||
u8 spattern = 0;
|
||||
u8 smask = 0;
|
||||
bool msb = false;
|
||||
|
||||
pattern.reserve(static_cast<size_t>(pattern_str.length()) / 2);
|
||||
mask.reserve(static_cast<size_t>(pattern_str.length()) / 2);
|
||||
|
||||
for (int i = 0; i < pattern_str.length(); i++)
|
||||
{
|
||||
const QChar ch = pattern_str[i];
|
||||
if (ch == ' ')
|
||||
continue;
|
||||
|
||||
if (ch == '?')
|
||||
{
|
||||
spattern = (spattern << 4);
|
||||
smask = (smask << 4);
|
||||
}
|
||||
else if (ch.isDigit())
|
||||
{
|
||||
spattern = (spattern << 4) | static_cast<u8>(ch.digitValue());
|
||||
smask = (smask << 4) | 0xF;
|
||||
}
|
||||
else if (ch.unicode() >= 'a' && ch.unicode() <= 'f')
|
||||
{
|
||||
spattern = (spattern << 4) | (0xA + static_cast<u8>(ch.unicode() - 'a'));
|
||||
smask = (smask << 4) | 0xF;
|
||||
}
|
||||
else if (ch.unicode() >= 'A' && ch.unicode() <= 'F')
|
||||
{
|
||||
spattern = (spattern << 4) | (0xA + static_cast<u8>(ch.unicode() - 'A'));
|
||||
smask = (smask << 4) | 0xF;
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::critical(this, windowTitle(),
|
||||
tr("Invalid search pattern. It should contain hex digits or question marks."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (msb)
|
||||
{
|
||||
pattern.push_back(spattern);
|
||||
mask.push_back(smask);
|
||||
spattern = 0;
|
||||
smask = 0;
|
||||
}
|
||||
|
||||
msb = !msb;
|
||||
}
|
||||
|
||||
if (msb)
|
||||
{
|
||||
// partial byte on the end
|
||||
spattern = (spattern << 4);
|
||||
smask = (smask << 4);
|
||||
pattern.push_back(spattern);
|
||||
mask.push_back(smask);
|
||||
}
|
||||
|
||||
if (pattern.empty())
|
||||
{
|
||||
QMessageBox::critical(this, windowTitle(),
|
||||
tr("Invalid search pattern. It should contain hex digits or question marks."));
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<PhysicalMemoryAddress> found_address =
|
||||
Bus::SearchMemory(m_next_memory_search_address, pattern.data(), mask.data(), static_cast<u32>(pattern.size()));
|
||||
bool wrapped_around = false;
|
||||
if (!found_address.has_value())
|
||||
{
|
||||
found_address = Bus::SearchMemory(0, pattern.data(), mask.data(), static_cast<u32>(pattern.size()));
|
||||
if (!found_address.has_value())
|
||||
{
|
||||
QMessageBox::critical(this, windowTitle(), tr("Pattern not found in memory."));
|
||||
return;
|
||||
}
|
||||
|
||||
wrapped_around = true;
|
||||
}
|
||||
|
||||
m_next_memory_search_address = found_address.value() + 1;
|
||||
if (scrollToMemoryAddress(found_address.value()))
|
||||
{
|
||||
const size_t highlight_offset = found_address.value() - m_ui.memoryView->addressOffset();
|
||||
m_ui.memoryView->setHighlightRange(highlight_offset, highlight_offset + pattern.size());
|
||||
}
|
||||
|
||||
if (wrapped_around)
|
||||
{
|
||||
QMessageBox::information(this, windowTitle(),
|
||||
tr("Pattern found at 0x%1 (passed the end of memory).")
|
||||
.arg(static_cast<uint>(found_address.value()), 8, 16, static_cast<QChar>('0')));
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::information(
|
||||
this, windowTitle(),
|
||||
tr("Pattern found at 0x%1.").arg(static_cast<uint>(found_address.value()), 8, 16, static_cast<QChar>('0')));
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::onMemorySearchStringChanged(const QString&)
|
||||
{
|
||||
m_next_memory_search_address = 0;
|
||||
}
|
||||
|
||||
QString MemoryEditorWindow::formatAddress(VirtualMemoryAddress address)
|
||||
{
|
||||
return QString::asprintf("0x%08X", static_cast<uint>(address));
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::setIfChanged(QLineEdit* const widget, const QString& text)
|
||||
{
|
||||
if (widget->text() == text)
|
||||
return;
|
||||
|
||||
widget->setText(text);
|
||||
widget->setCursorPosition(0);
|
||||
}
|
||||
|
||||
QString MemoryEditorWindow::formatNumber(u64 value, bool is_signed, int byte_size) const
|
||||
{
|
||||
QString ret;
|
||||
|
||||
QString prefix;
|
||||
int base;
|
||||
int width;
|
||||
|
||||
if (m_ui.dataInspectorOctal->isChecked())
|
||||
{
|
||||
prefix = QStringLiteral("0");
|
||||
base = 8;
|
||||
width = 0;
|
||||
}
|
||||
else if (m_ui.dataInspectorHexadecimal->isChecked())
|
||||
{
|
||||
prefix = QStringLiteral("0x");
|
||||
base = 16;
|
||||
width = byte_size * 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// prefix = QString();
|
||||
base = 10;
|
||||
width = 0;
|
||||
}
|
||||
|
||||
switch (byte_size)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
if (is_signed)
|
||||
{
|
||||
if (static_cast<s8>(value) < 0)
|
||||
{
|
||||
ret = QStringLiteral("%1").arg(static_cast<s8>(value), width, base, QChar('0'));
|
||||
ret.insert(1, prefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<s8>(value), width, base, QChar('0'));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<u8>(value), width, base, QChar('0'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
{
|
||||
if (is_signed)
|
||||
{
|
||||
if (static_cast<s16>(value) < 0)
|
||||
{
|
||||
ret = QStringLiteral("%1").arg(static_cast<s16>(value), width, base, QChar('0'));
|
||||
ret.insert(1, prefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<s16>(value), width, base, QChar('0'));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<u16>(value), width, base, QChar('0'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
{
|
||||
if (is_signed)
|
||||
{
|
||||
if (static_cast<s32>(value) < 0)
|
||||
{
|
||||
ret = QStringLiteral("%1").arg(static_cast<s32>(value), width, base, QChar('0'));
|
||||
ret.insert(1, prefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<s32>(value), width, base, QChar('0'));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<u32>(value), width, base, QChar('0'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 8:
|
||||
{
|
||||
if (is_signed)
|
||||
{
|
||||
if (static_cast<s64>(value) < 0)
|
||||
{
|
||||
ret = QStringLiteral("%1").arg(static_cast<s64>(value), width, base, QChar('0'));
|
||||
ret.insert(1, prefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<s64>(value), width, base, QChar('0'));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<u64>(value), width, base, QChar('0'));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::setupAdditionalUi()
|
||||
{
|
||||
setWindowIcon(QtHost::GetAppIcon());
|
||||
|
||||
#ifdef _WIN32
|
||||
QFont fixedFont;
|
||||
fixedFont.setFamily(QStringLiteral("Consolas"));
|
||||
fixedFont.setFixedPitch(true);
|
||||
fixedFont.setStyleHint(QFont::TypeWriter);
|
||||
fixedFont.setPointSize(10);
|
||||
#else
|
||||
QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
||||
|
||||
#ifdef __linux__
|
||||
// Fonts on Linux tend to be wider, so reduce the size.
|
||||
// Otherwise the memory view gets cut off.
|
||||
fixedFont.setPointSize(9);
|
||||
#endif
|
||||
#endif
|
||||
m_ui.memoryView->setFont(fixedFont);
|
||||
|
||||
QtUtils::RestoreWindowGeometry("MemoryEditorWindow", this);
|
||||
|
||||
// Set minimum width for data inspector.
|
||||
m_ui.dataInspectorAddress->setFont(fixedFont);
|
||||
m_ui.dataInspectorUnsignedByte->setFont(fixedFont);
|
||||
m_ui.dataInspectorSignedByte->setFont(fixedFont);
|
||||
m_ui.dataInspectorUnsignedHalfword->setFont(fixedFont);
|
||||
m_ui.dataInspectorSignedHalfword->setFont(fixedFont);
|
||||
m_ui.dataInspectorUnsignedWord->setFont(fixedFont);
|
||||
m_ui.dataInspectorSignedWord->setFont(fixedFont);
|
||||
m_ui.dataInspectorUnsignedDoubleWord->setFont(fixedFont);
|
||||
m_ui.dataInspectorSignedDoubleWord->setFont(fixedFont);
|
||||
m_ui.dataInspectorFloat32->setFont(fixedFont);
|
||||
m_ui.dataInspectorFloat64->setFont(fixedFont);
|
||||
m_ui.dataInspectorASCIICharacter->setFont(fixedFont);
|
||||
m_ui.dataInspectorUTF8String->setFont(fixedFont);
|
||||
m_ui.dataInspectorSignedDoubleWord->setMinimumWidth(
|
||||
QFontMetrics(fixedFont).size(0, QStringLiteral("-8888888888888888888888")).width());
|
||||
|
||||
// Default selection.
|
||||
m_ui.memoryRegionRAM->setChecked(true);
|
||||
m_ui.dataInspectorHexadecimal->setChecked(true);
|
||||
m_ui.dataInspectorLittleEndian->setChecked(true);
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::connectSignals()
|
||||
{
|
||||
connect(g_emu_thread, &EmuThread::systemPaused, this, &MemoryEditorWindow::onSystemPaused);
|
||||
connect(g_emu_thread, &EmuThread::systemResumed, this, &MemoryEditorWindow::onSystemResumed);
|
||||
connect(g_emu_thread, &EmuThread::systemStarted, this, &MemoryEditorWindow::onSystemStarted);
|
||||
connect(g_emu_thread, &EmuThread::systemDestroyed, this, &MemoryEditorWindow::onSystemDestroyed);
|
||||
|
||||
connect(m_ui.address, &QLineEdit::editingFinished, this, &MemoryEditorWindow::onAddressEditingFinished);
|
||||
|
||||
connect(m_ui.memoryView, &MemoryViewWidget::topAddressChanged, this,
|
||||
&MemoryEditorWindow::onMemoryViewTopAddressChanged);
|
||||
connect(m_ui.memoryView, &MemoryViewWidget::selectedAddressChanged, this, &MemoryEditorWindow::updateDataInspector);
|
||||
connect(m_ui.memoryRegionButtonGroup, &QButtonGroup::buttonToggled, this,
|
||||
&MemoryEditorWindow::onMemoryRegionButtonToggled);
|
||||
connect(m_ui.endianButtonGroup, &QButtonGroup::buttonToggled, this,
|
||||
&MemoryEditorWindow::onDataInspectorEndianButtonToggled);
|
||||
connect(m_ui.baseButtonGroup, &QButtonGroup::buttonToggled, this,
|
||||
&MemoryEditorWindow::onDataInspectorBaseButtonToggled);
|
||||
|
||||
connect(m_ui.memorySearch, &QPushButton::clicked, this, &MemoryEditorWindow::onMemorySearchTriggered);
|
||||
connect(m_ui.memorySearchString, &QLineEdit::textChanged, this, &MemoryEditorWindow::onMemorySearchStringChanged);
|
||||
|
||||
connect(&m_refresh_timer, &QTimer::timeout, this, &MemoryEditorWindow::timerRefresh);
|
||||
m_refresh_timer.setInterval(TIMER_REFRESH_INTERVAL_MS);
|
||||
|
||||
m_go_shortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_G), this);
|
||||
connect(m_go_shortcut, &QShortcut::activated, this, &MemoryEditorWindow::onDumpAddressTriggered);
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::updateUIEnabled()
|
||||
{
|
||||
const bool system_valid = QtHost::IsSystemValid();
|
||||
|
||||
m_ui.memoryView->setEnabled(system_valid);
|
||||
m_ui.address->setEnabled(system_valid);
|
||||
m_ui.memoryRegionRAM->setEnabled(system_valid);
|
||||
m_ui.memoryRegionEXP1->setEnabled(system_valid);
|
||||
m_ui.memoryRegionScratchpad->setEnabled(system_valid);
|
||||
m_ui.memoryRegionBIOS->setEnabled(system_valid);
|
||||
m_ui.memorySearch->setEnabled(system_valid);
|
||||
m_ui.memorySearchString->setEnabled(system_valid);
|
||||
m_ui.dataInspector->setEnabled(system_valid);
|
||||
m_go_shortcut->setEnabled(system_valid);
|
||||
|
||||
// Partial/timer refreshes only active when not paused.
|
||||
const bool timer_active = system_valid && !QtHost::IsSystemPaused();
|
||||
if (m_refresh_timer.isActive() != timer_active)
|
||||
timer_active ? m_refresh_timer.start() : m_refresh_timer.stop();
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
QtUtils::SaveWindowGeometry("MemoryEditorWindow", this);
|
||||
QWidget::closeEvent(event);
|
||||
emit closed();
|
||||
}
|
||||
|
||||
bool MemoryEditorWindow::scrollToMemoryAddress(VirtualMemoryAddress address)
|
||||
{
|
||||
const PhysicalMemoryAddress phys_address = CPU::VirtualAddressToPhysical(address);
|
||||
std::optional<Bus::MemoryRegion> region = Bus::GetMemoryRegionForAddress(phys_address);
|
||||
if (!region.has_value())
|
||||
return false;
|
||||
|
||||
if (region.value() == Bus::MemoryRegion::EXP1)
|
||||
m_ui.memoryRegionEXP1->setChecked(true);
|
||||
else if (region.value() == Bus::MemoryRegion::Scratchpad)
|
||||
m_ui.memoryRegionScratchpad->setChecked(true);
|
||||
else if (region.value() == Bus::MemoryRegion::BIOS)
|
||||
m_ui.memoryRegionBIOS->setChecked(true);
|
||||
else
|
||||
m_ui.memoryRegionRAM->setChecked(true);
|
||||
|
||||
const PhysicalMemoryAddress offset = phys_address - Bus::GetMemoryRegionStart(region.value());
|
||||
m_ui.memoryView->scrollToOffset(offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::updateMemoryViewRegion()
|
||||
{
|
||||
Bus::MemoryRegion region;
|
||||
if (m_ui.memoryRegionEXP1->isChecked())
|
||||
region = Bus::MemoryRegion::EXP1;
|
||||
else if (m_ui.memoryRegionScratchpad->isChecked())
|
||||
region = Bus::MemoryRegion::Scratchpad;
|
||||
else if (m_ui.memoryRegionBIOS->isChecked())
|
||||
region = Bus::MemoryRegion::BIOS;
|
||||
else
|
||||
region = Bus::MemoryRegion::RAM;
|
||||
|
||||
static constexpr auto edit_ram_callback = [](size_t offset, size_t count) {
|
||||
// shouldn't happen
|
||||
if (offset > Bus::g_ram_size)
|
||||
return;
|
||||
|
||||
const u32 start_page = static_cast<u32>(offset) >> HOST_PAGE_SHIFT;
|
||||
const u32 end_page = static_cast<u32>(offset + count - 1) >> HOST_PAGE_SHIFT;
|
||||
Host::RunOnCPUThread([start_page, end_page]() {
|
||||
for (u32 i = start_page; i <= end_page; i++)
|
||||
{
|
||||
if (Bus::g_ram_code_bits[i])
|
||||
CPU::CodeCache::InvalidateBlocksWithPageIndex(i);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const PhysicalMemoryAddress start = Bus::GetMemoryRegionStart(region);
|
||||
const PhysicalMemoryAddress end = Bus::GetMemoryRegionEnd(region);
|
||||
void* const mem_ptr = Bus::GetMemoryRegionPointer(region);
|
||||
const bool mem_writable = Bus::IsMemoryRegionWritable(region);
|
||||
const MemoryViewWidget::EditCallback edit_callback =
|
||||
((region == Bus::MemoryRegion::RAM) ? static_cast<MemoryViewWidget::EditCallback>(edit_ram_callback) : nullptr);
|
||||
m_ui.memoryView->setData(start, mem_ptr, end - start, mem_writable, edit_callback);
|
||||
}
|
||||
|
||||
void MemoryEditorWindow::updateDataInspector()
|
||||
{
|
||||
const size_t address = m_ui.memoryView->selectedAddress();
|
||||
if (address == MemoryViewWidget::INVALID_SELECTED_ADDRESS)
|
||||
{
|
||||
m_ui.dataInspectorAddress->clear();
|
||||
m_ui.dataInspectorUnsignedByte->clear();
|
||||
m_ui.dataInspectorSignedByte->clear();
|
||||
m_ui.dataInspectorUnsignedHalfword->clear();
|
||||
m_ui.dataInspectorSignedHalfword->clear();
|
||||
m_ui.dataInspectorUnsignedWord->clear();
|
||||
m_ui.dataInspectorSignedWord->clear();
|
||||
m_ui.dataInspectorUnsignedDoubleWord->clear();
|
||||
m_ui.dataInspectorSignedDoubleWord->clear();
|
||||
m_ui.dataInspectorFloat32->clear();
|
||||
m_ui.dataInspectorFloat64->clear();
|
||||
m_ui.dataInspectorASCIICharacter->clear();
|
||||
m_ui.dataInspectorUTF8String->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
u8 value8 = 0;
|
||||
u16 value16 = 0;
|
||||
u32 value32 = 0;
|
||||
u32 value64_high = 0;
|
||||
u64 value64 = 0;
|
||||
CPU::SafeReadMemoryWord(static_cast<VirtualMemoryAddress>(address), &value32);
|
||||
CPU::SafeReadMemoryWord(static_cast<VirtualMemoryAddress>(address), &value64_high);
|
||||
value64 = (ZeroExtend64(value64_high) << 32) | ZeroExtend64(value32);
|
||||
|
||||
const bool big_endian = m_ui.dataInspectorBigEndian->isChecked();
|
||||
if (big_endian)
|
||||
{
|
||||
value8 = Truncate8(value32);
|
||||
value16 = ByteSwap(Truncate16(value32));
|
||||
value32 = ByteSwap(value32);
|
||||
value64 = ByteSwap(value64);
|
||||
}
|
||||
else
|
||||
{
|
||||
value8 = Truncate8(value32);
|
||||
value16 = Truncate16(value32);
|
||||
}
|
||||
|
||||
setIfChanged(m_ui.dataInspectorAddress, formatAddress(static_cast<VirtualMemoryAddress>(address)));
|
||||
setIfChanged(m_ui.dataInspectorUnsignedByte, formatNumber(value8, false, 1));
|
||||
setIfChanged(m_ui.dataInspectorSignedByte, formatNumber(value8, true, 1));
|
||||
setIfChanged(m_ui.dataInspectorUnsignedHalfword, formatNumber(value16, false, 2));
|
||||
setIfChanged(m_ui.dataInspectorSignedHalfword, formatNumber(value16, true, 2));
|
||||
setIfChanged(m_ui.dataInspectorUnsignedWord, formatNumber(value32, false, 4));
|
||||
setIfChanged(m_ui.dataInspectorSignedWord, formatNumber(value32, true, 4));
|
||||
setIfChanged(m_ui.dataInspectorUnsignedDoubleWord, formatNumber(value64, false, 8));
|
||||
setIfChanged(m_ui.dataInspectorSignedDoubleWord, formatNumber(value64, true, 8));
|
||||
setIfChanged(m_ui.dataInspectorFloat32, QString::number(std::bit_cast<float>(value32)));
|
||||
setIfChanged(m_ui.dataInspectorFloat64, QString::number(std::bit_cast<double>(value64)));
|
||||
|
||||
if (value8 >= 0x20 && value8 <= 0x7E)
|
||||
{
|
||||
m_ui.dataInspectorASCIICharacter->setText(QStringLiteral("'%1'").arg(static_cast<QChar>(value8)));
|
||||
|
||||
SmallString str;
|
||||
CPU::SafeReadMemoryCString(static_cast<VirtualMemoryAddress>(address), &str, 32);
|
||||
|
||||
// only display printable characters
|
||||
for (size_t i = 0; i < str.length(); i++)
|
||||
{
|
||||
if (str[i] < 0x20 || str[i] > 0x7E)
|
||||
{
|
||||
str.resize(static_cast<u32>(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!str.empty())
|
||||
{
|
||||
str.prepend('\"');
|
||||
str.append('\"');
|
||||
setIfChanged(m_ui.dataInspectorUTF8String, QtUtils::StringViewToQString(str));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ui.dataInspectorUTF8String->clear();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setIfChanged(m_ui.dataInspectorASCIICharacter,
|
||||
QStringLiteral("'\\x%1'").arg(static_cast<uint>(value8), 2, 16, QChar('0')));
|
||||
m_ui.dataInspectorUTF8String->clear();
|
||||
}
|
||||
}
|
||||
69
src/duckstation-qt/memoryeditorwindow.h
Normal file
69
src/duckstation-qt/memoryeditorwindow.h
Normal file
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_memoryeditorwindow.h"
|
||||
|
||||
#include "core/types.h"
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtWidgets/QWidget>
|
||||
#include <optional>
|
||||
|
||||
class QShortcut;
|
||||
|
||||
class MemoryEditorWindow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MemoryEditorWindow(QWidget* parent = nullptr);
|
||||
~MemoryEditorWindow();
|
||||
|
||||
bool scrollToMemoryAddress(VirtualMemoryAddress address);
|
||||
|
||||
Q_SIGNALS:
|
||||
void closed();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event);
|
||||
|
||||
private:
|
||||
void setupAdditionalUi();
|
||||
void connectSignals();
|
||||
void updateUIEnabled();
|
||||
|
||||
void onSystemStarted();
|
||||
void onSystemDestroyed();
|
||||
void onSystemPaused();
|
||||
void onSystemResumed();
|
||||
|
||||
void timerRefresh();
|
||||
void refreshAll();
|
||||
void updateMemoryViewRegion();
|
||||
void updateDataInspector();
|
||||
|
||||
void onMemoryViewTopAddressChanged(size_t address);
|
||||
|
||||
void onAddressEditingFinished();
|
||||
void onDumpAddressTriggered();
|
||||
void onMemoryRegionButtonToggled(QAbstractButton*, bool checked);
|
||||
void onDataInspectorBaseButtonToggled(QAbstractButton*, bool checked);
|
||||
void onDataInspectorEndianButtonToggled(QAbstractButton*, bool checked);
|
||||
void onMemorySearchTriggered();
|
||||
void onMemorySearchStringChanged(const QString&);
|
||||
|
||||
QString formatNumber(u64 value, bool is_signed, int byte_size) const;
|
||||
|
||||
static QString formatAddress(VirtualMemoryAddress address);
|
||||
static void setIfChanged(QLineEdit* const widget, const QString& text);
|
||||
|
||||
Ui::MemoryEditorWindow m_ui;
|
||||
|
||||
QShortcut* m_go_shortcut = nullptr;
|
||||
|
||||
QTimer m_refresh_timer;
|
||||
|
||||
PhysicalMemoryAddress m_next_memory_search_address = 0;
|
||||
};
|
||||
422
src/duckstation-qt/memoryeditorwindow.ui
Normal file
422
src/duckstation-qt/memoryeditorwindow.ui
Normal file
@@ -0,0 +1,422 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MemoryEditorWindow</class>
|
||||
<widget class="QWidget" name="MemoryEditorWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1130</width>
|
||||
<height>474</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Memory Editor</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Address:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="address"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="MemoryViewWidget" name="memoryView" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="memoryRegionRAM">
|
||||
<property name="text">
|
||||
<string>RAM</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">memoryRegionButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="memoryRegionScratchpad">
|
||||
<property name="text">
|
||||
<string>Scratchpad</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">memoryRegionButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="memoryRegionEXP1">
|
||||
<property name="text">
|
||||
<string>EXP1</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">memoryRegionButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="memoryRegionBIOS">
|
||||
<property name="text">
|
||||
<string>BIOS</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">memoryRegionButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="memorySearchString"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="memorySearch">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="dataInspector">
|
||||
<property name="title">
|
||||
<string>Data Inspector</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="dataInspectorLayout">
|
||||
<item row="11" column="0">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>ASCII Character:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorUnsignedWord">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorUTF8String">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorUnsignedHalfword">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Signed Byte:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorSignedByte">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="14" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="dataInspectorDecimal">
|
||||
<property name="text">
|
||||
<string>Decimal</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">baseButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="dataInspectorHexadecimal">
|
||||
<property name="text">
|
||||
<string>Hexadecimal</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">baseButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="dataInspectorOctal">
|
||||
<property name="text">
|
||||
<string>Octal</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">baseButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Unsigned Byte:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="dataInspectorLittleEndian">
|
||||
<property name="text">
|
||||
<string>Little Endian</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">endianButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="dataInspectorBigEndian">
|
||||
<property name="text">
|
||||
<string>Big Endian</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">endianButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Unsigned Halfword:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>32-Bit Float:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorUnsignedByte">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorAddress">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_13">
|
||||
<property name="text">
|
||||
<string>Unsigned Doubleword:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorUnsignedDoubleWord">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorFloat32">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorSignedHalfword">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorSignedWord">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Address:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorFloat64">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="0">
|
||||
<widget class="QLabel" name="label_12">
|
||||
<property name="text">
|
||||
<string>UTF-8 String:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>64-Bit Float:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorASCIICharacter">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Unsigned Word:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Signed Word:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="0" colspan="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Signed Halfword:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_14">
|
||||
<property name="text">
|
||||
<string>Signed Doubleword:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QLineEdit" name="dataInspectorSignedDoubleWord">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>MemoryViewWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>duckstation-qt/memoryviewwidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="resources/duckstation-qt.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
<buttongroups>
|
||||
<buttongroup name="baseButtonGroup"/>
|
||||
<buttongroup name="memoryRegionButtonGroup"/>
|
||||
<buttongroup name="endianButtonGroup"/>
|
||||
</buttongroups>
|
||||
</ui>
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <QtWidgets/QScrollBar>
|
||||
#include <cstring>
|
||||
|
||||
#include "moc_memoryviewwidget.cpp"
|
||||
|
||||
MemoryViewWidget::MemoryViewWidget(QWidget* parent /* = nullptr */, size_t address_offset /* = 0 */,
|
||||
void* data_ptr /* = nullptr */, size_t data_size /* = 0 */,
|
||||
bool data_editable /* = false */, EditCallback edit_callback /* = nullptr */)
|
||||
@@ -46,6 +48,17 @@ void MemoryViewWidget::updateMetrics()
|
||||
m_char_height = fm.height();
|
||||
}
|
||||
|
||||
size_t MemoryViewWidget::selectedAddress() const
|
||||
{
|
||||
return (m_selected_address != INVALID_SELECTED_ADDRESS) ? (m_selected_address + m_address_offset) :
|
||||
INVALID_SELECTED_ADDRESS;
|
||||
}
|
||||
|
||||
size_t MemoryViewWidget::topAddress() const
|
||||
{
|
||||
return static_cast<size_t>(verticalScrollBar()->value()) * m_bytes_per_line + m_address_offset;
|
||||
}
|
||||
|
||||
void MemoryViewWidget::setData(size_t address_offset, void* data_ptr, size_t data_size, bool data_editable,
|
||||
EditCallback edit_callback)
|
||||
{
|
||||
@@ -130,6 +143,7 @@ void MemoryViewWidget::keyPressEvent(QKeyEvent* event)
|
||||
m_selected_address--;
|
||||
m_editing_nibble = -1;
|
||||
forceRefresh();
|
||||
notifySelectedAddressChanged();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -152,6 +166,7 @@ void MemoryViewWidget::keyPressEvent(QKeyEvent* event)
|
||||
|
||||
m_selected_address = std::min(m_selected_address + 1, m_data_size - 1);
|
||||
forceRefresh();
|
||||
notifySelectedAddressChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -182,6 +197,7 @@ void MemoryViewWidget::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
m_editing_nibble = -1;
|
||||
m_selected_address = std::min(m_selected_address + 1, m_data_size - 1);
|
||||
notifySelectedAddressChanged();
|
||||
}
|
||||
|
||||
forceRefresh();
|
||||
@@ -225,6 +241,7 @@ void MemoryViewWidget::keyPressEvent(QKeyEvent* event)
|
||||
forceRefresh();
|
||||
expandCurrentDataToInclude(m_selected_address);
|
||||
adjustScrollToInclude(m_selected_address);
|
||||
notifySelectedAddressChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -448,6 +465,7 @@ void MemoryViewWidget::setSelection(size_t new_selection, bool new_ascii)
|
||||
m_selection_was_ascii = new_ascii;
|
||||
m_editing_nibble = -1;
|
||||
forceRefresh();
|
||||
notifySelectedAddressChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,12 +526,7 @@ void MemoryViewWidget::forceRefresh()
|
||||
void MemoryViewWidget::adjustContent()
|
||||
{
|
||||
if (!m_data)
|
||||
{
|
||||
setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setEnabled(true);
|
||||
|
||||
int w = addressWidth() + hexWidth() + asciiWidth();
|
||||
horizontalScrollBar()->setRange(0, w - viewport()->width());
|
||||
@@ -534,4 +547,11 @@ void MemoryViewWidget::adjustContent()
|
||||
expandCurrentDataToInclude(m_end_offset);
|
||||
|
||||
forceRefresh();
|
||||
}
|
||||
|
||||
emit topAddressChanged(topAddress());
|
||||
}
|
||||
|
||||
void MemoryViewWidget::notifySelectedAddressChanged()
|
||||
{
|
||||
emit selectedAddressChanged(selectedAddress());
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
|
||||
class MemoryViewWidget : public QAbstractScrollArea
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static constexpr size_t INVALID_SELECTED_ADDRESS = ~static_cast<size_t>(0);
|
||||
|
||||
using EditCallback = void (*)(size_t offset, size_t bytes);
|
||||
|
||||
MemoryViewWidget(QWidget* parent = nullptr, size_t address_offset = 0, void* data_ptr = nullptr, size_t data_size = 0,
|
||||
@@ -17,6 +21,8 @@ public:
|
||||
~MemoryViewWidget();
|
||||
|
||||
size_t addressOffset() const { return m_address_offset; }
|
||||
size_t selectedAddress() const;
|
||||
size_t topAddress() const;
|
||||
|
||||
void setData(size_t address_offset, void* data_ptr, size_t data_size, bool data_editable, EditCallback edit_callback);
|
||||
void setHighlightRange(size_t start, size_t end);
|
||||
@@ -28,6 +34,10 @@ public:
|
||||
void saveCurrentData();
|
||||
void forceRefresh();
|
||||
|
||||
Q_SIGNALS:
|
||||
void topAddressChanged(size_t address);
|
||||
void selectedAddressChanged(size_t address);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event);
|
||||
void resizeEvent(QResizeEvent* event);
|
||||
@@ -36,8 +46,6 @@ protected:
|
||||
void keyPressEvent(QKeyEvent* event);
|
||||
|
||||
private:
|
||||
static constexpr size_t INVALID_SELECTED_ADDRESS = ~static_cast<size_t>(0);
|
||||
|
||||
int addressWidth() const;
|
||||
int hexWidth() const;
|
||||
int asciiWidth() const;
|
||||
@@ -47,6 +55,7 @@ private:
|
||||
void expandCurrentDataToInclude(size_t offset);
|
||||
void adjustScrollToInclude(size_t offset);
|
||||
void adjustContent();
|
||||
void notifySelectedAddressChanged();
|
||||
|
||||
void* m_data;
|
||||
size_t m_data_size;
|
||||
|
||||
Reference in New Issue
Block a user