From 81fd7e3c1a811e0dea0c8995143c4c7f899fd672 Mon Sep 17 00:00:00 2001
From: Vicki Pfau <vi@endrift.com>
Date: Sun, 22 Sep 2019 20:21:04 -0700
Subject: [PATCH] Qt: Memory range dumping (closes #1298)

---
 CHANGES                        |   1 +
 src/gb/core.c                  |   6 +-
 src/platform/qt/CMakeLists.txt |   2 +
 src/platform/qt/MemoryDump.cpp | 120 +++++++++++++++++++++++++
 src/platform/qt/MemoryDump.h   |  38 ++++++++
 src/platform/qt/MemoryDump.ui  | 159 +++++++++++++++++++++++++++++++++
 src/platform/qt/MemoryView.cpp |  12 +++
 src/platform/qt/MemoryView.h   |   1 +
 src/platform/qt/MemoryView.ui  |  18 +++-
 9 files changed, 352 insertions(+), 5 deletions(-)
 create mode 100644 src/platform/qt/MemoryDump.cpp
 create mode 100644 src/platform/qt/MemoryDump.h
 create mode 100644 src/platform/qt/MemoryDump.ui

diff --git a/CHANGES b/CHANGES
index 78f983c68..3ed7e3302 100644
--- a/CHANGES
+++ b/CHANGES
@@ -21,6 +21,7 @@ Features:
  - Qt: Add export button for tile view (closes mgba.io/i/1507)
  - Qt: Add recent game list clearing (closes mgba.io/i/1380)
  - GB: Yanking gamepak now supported
+ - Qt: Memory range dumping (closes mgba.io/i/1298)
 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/gb/core.c b/src/gb/core.c
index 655449eaf..25d356143 100644
--- a/src/gb/core.c
+++ b/src/gb/core.c
@@ -40,7 +40,7 @@ static const struct mCoreChannelInfo _GBAudioChannels[] = {
 
 static const struct mCoreMemoryBlock _GBMemoryBlocks[] = {
 	{ -1, "mem", "All", "All", 0, 0x10000, 0x10000, mCORE_MEMORY_VIRTUAL },
-	{ GB_REGION_CART_BANK0, "cart0", "ROM Bank", "Game Pak (32kiB)", GB_BASE_CART_BANK0, GB_SIZE_CART_BANK0 * 2, 0x800000, mCORE_MEMORY_READ | mCORE_MEMORY_MAPPED, 511 },
+	{ GB_REGION_CART_BANK0, "cart0", "ROM Bank", "Game Pak (32kiB)", GB_BASE_CART_BANK0, GB_BASE_CART_BANK0 + GB_SIZE_CART_BANK0 * 2, 0x800000, mCORE_MEMORY_READ | mCORE_MEMORY_MAPPED, 511, GB_BASE_CART_BANK0 + GB_SIZE_CART_BANK0 },
 	{ GB_REGION_VRAM, "vram", "VRAM", "Video RAM (8kiB)", GB_BASE_VRAM, GB_BASE_VRAM + GB_SIZE_VRAM, GB_SIZE_VRAM, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED },
 	{ GB_REGION_EXTERNAL_RAM, "sram", "SRAM", "External RAM (8kiB)", GB_BASE_EXTERNAL_RAM, GB_BASE_EXTERNAL_RAM + GB_SIZE_EXTERNAL_RAM, GB_SIZE_EXTERNAL_RAM * 4, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED, 3 },
 	{ GB_REGION_WORKING_RAM_BANK0, "wram", "WRAM", "Working RAM (8kiB)", GB_BASE_WORKING_RAM_BANK0, GB_BASE_WORKING_RAM_BANK0 + GB_SIZE_WORKING_RAM_BANK0 * 2 , GB_SIZE_WORKING_RAM_BANK0 * 2, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED },
@@ -51,10 +51,10 @@ static const struct mCoreMemoryBlock _GBMemoryBlocks[] = {
 
 static const struct mCoreMemoryBlock _GBCMemoryBlocks[] = {
 	{ -1, "mem", "All", "All", 0, 0x10000, 0x10000, mCORE_MEMORY_VIRTUAL },
-	{ GB_REGION_CART_BANK0, "cart0", "ROM Bank", "Game Pak (32kiB)", GB_BASE_CART_BANK0, GB_SIZE_CART_BANK0 * 2, 0x800000, mCORE_MEMORY_READ | mCORE_MEMORY_MAPPED, 511 },
+	{ GB_REGION_CART_BANK0, "cart0", "ROM Bank", "Game Pak (32kiB)", GB_BASE_CART_BANK0, GB_BASE_CART_BANK0 + GB_SIZE_CART_BANK0 * 2, 0x800000, mCORE_MEMORY_READ | mCORE_MEMORY_MAPPED, 511, GB_BASE_CART_BANK0 + GB_SIZE_CART_BANK0 },
 	{ GB_REGION_VRAM, "vram", "VRAM", "Video RAM (8kiB)", GB_BASE_VRAM, GB_BASE_VRAM + GB_SIZE_VRAM, GB_SIZE_VRAM * 2, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED, 1 },
 	{ GB_REGION_EXTERNAL_RAM, "sram", "SRAM", "External RAM (8kiB)", GB_BASE_EXTERNAL_RAM, GB_BASE_EXTERNAL_RAM + GB_SIZE_EXTERNAL_RAM, GB_SIZE_EXTERNAL_RAM * 4, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED, 3 },
-	{ GB_REGION_WORKING_RAM_BANK0, "wram", "WRAM", "Working RAM (8kiB)", GB_BASE_WORKING_RAM_BANK0, GB_BASE_WORKING_RAM_BANK0 + GB_SIZE_WORKING_RAM_BANK0 * 2, GB_SIZE_WORKING_RAM_BANK0 * 8, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED, 7 },
+	{ GB_REGION_WORKING_RAM_BANK0, "wram", "WRAM", "Working RAM (8kiB)", GB_BASE_WORKING_RAM_BANK0, GB_BASE_WORKING_RAM_BANK0 + GB_SIZE_WORKING_RAM_BANK0 * 2, GB_SIZE_WORKING_RAM_BANK0 * 8, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED, 7, GB_BASE_WORKING_RAM_BANK0 + GB_SIZE_WORKING_RAM_BANK0 },
 	{ GB_BASE_OAM, "oam", "OAM", "OBJ Attribute Memory", GB_BASE_OAM, GB_BASE_OAM + GB_SIZE_OAM, GB_SIZE_OAM, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED },
 	{ GB_BASE_IO, "io", "MMIO", "Memory-Mapped I/O", GB_BASE_IO, GB_BASE_IO + GB_SIZE_IO, GB_SIZE_IO, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED },
 	{ GB_BASE_HRAM, "hram", "HRAM", "High RAM", GB_BASE_HRAM, GB_BASE_HRAM + GB_SIZE_HRAM, GB_SIZE_HRAM, mCORE_MEMORY_RW | mCORE_MEMORY_MAPPED },
diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt
index 32fa95842..0c681c75c 100644
--- a/src/platform/qt/CMakeLists.txt
+++ b/src/platform/qt/CMakeLists.txt
@@ -85,6 +85,7 @@ set(SOURCE_FILES
 	LogConfigModel.cpp
 	LogView.cpp
 	MapView.cpp
+	MemoryDump.cpp
 	MemoryModel.cpp
 	MemorySearch.cpp
 	MemoryView.cpp
@@ -126,6 +127,7 @@ set(UI_FILES
 	LoadSaveState.ui
 	LogView.ui
 	MapView.ui
+	MemoryDump.ui
 	MemorySearch.ui
 	MemoryView.ui
 	ObjView.ui
diff --git a/src/platform/qt/MemoryDump.cpp b/src/platform/qt/MemoryDump.cpp
new file mode 100644
index 000000000..982353ee2
--- /dev/null
+++ b/src/platform/qt/MemoryDump.cpp
@@ -0,0 +1,120 @@
+/* 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 "MemoryDump.h"
+
+#include "CoreController.h"
+#include "GBAApp.h"
+#include "LogController.h"
+
+using namespace QGBA;
+
+MemoryDump::MemoryDump(std::shared_ptr<CoreController> controller, QWidget* parent)
+	: QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint)
+	, m_controller(controller)
+{
+	m_ui.setupUi(this);
+
+	connect(this, &QDialog::accepted, this, &MemoryDump::save);
+}
+
+void MemoryDump::save() {
+	QString filename = GBAApp::app()->getSaveFileName(this, tr("Save memory region"));
+	if (filename.isNull()) {
+		return;
+	}
+	QFile outfile(filename);
+	if (!outfile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+		LOG(QT, WARN) << tr("Failed to open output file: %1").arg(filename);
+		return;
+	}
+	QByteArray out(serialize());
+	outfile.write(out);
+}
+
+void MemoryDump::setAddress(uint32_t start) {
+	m_ui.address->setValue(start);
+}
+
+void MemoryDump::setSegment(int seg) {
+	m_ui.segment->setValue(seg);
+}
+
+void MemoryDump::setByteCount(uint32_t count) {
+	m_ui.bytes->setValue(count);
+}
+
+QByteArray MemoryDump::serialize() {
+	CoreController::Interrupter interrupter(m_controller);
+	mCore* core = m_controller->thread()->core;
+	const mCoreMemoryBlock* blocks;
+	size_t nBlocks = core->listMemoryBlocks(core, &blocks);
+
+	int size = m_ui.bytes->value();
+	uint32_t start = m_ui.address->value();
+	int segment = m_ui.segment->value();
+	bool spanSegments = m_ui.spanSegments->isChecked();
+
+	QByteArray mem;
+	while (size) {
+		const mCoreMemoryBlock* bestMatch = NULL;
+		const char* block = NULL;
+		size_t blockSize = 0;
+		for (size_t i = 0; i < nBlocks; ++i) {
+			if (blocks[i].end <= start) {
+				continue;
+			}
+			if (blocks[i].start > start) {
+				continue;
+			}
+			block = static_cast<const char*>(core->getMemoryBlock(core, blocks[i].id, &blockSize));
+			if (block) {
+				bestMatch = &blocks[i];
+				break;
+			} else if (!bestMatch) {
+				bestMatch = &blocks[i];
+				blockSize = 0;
+			}
+		}
+		if (!spanSegments) {
+			blockSize = bestMatch->end - bestMatch->start;
+		} else if (!blockSize) {
+			blockSize = bestMatch->size;
+		}
+		size_t segmentSize = bestMatch->end - bestMatch->start;
+		if (bestMatch->segmentStart) {
+			segmentSize = bestMatch->segmentStart - bestMatch->start;
+		}
+		if (segment > 0) {
+			start += segment * segmentSize;
+		}
+		int maxFromRegion = blockSize - (start - bestMatch->start);
+		if (maxFromRegion <= 0) {
+			continue;
+		}
+		int fromRegion = std::min(size, maxFromRegion);
+		if (block && (segment >= 0 || segmentSize == blockSize)) {
+			block = &block[start - bestMatch->start];
+			mem.append(QByteArray::fromRawData(block, fromRegion));
+			size -= fromRegion;
+			start += fromRegion;
+			if (spanSegments) {
+				break;
+			}
+		} else {
+			for (int i = 0; i < 16; ++i) {
+				char datum = core->rawRead8(core, start, segment);
+				mem.append(datum);
+				++start;
+				--size;
+				if (!size) {
+					break;
+				}
+			}
+		}
+	}
+
+	return mem;
+}
\ No newline at end of file
diff --git a/src/platform/qt/MemoryDump.h b/src/platform/qt/MemoryDump.h
new file mode 100644
index 000000000..bfed5fe08
--- /dev/null
+++ b/src/platform/qt/MemoryDump.h
@@ -0,0 +1,38 @@
+/* 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_MemoryDump.h"
+
+#include <memory>
+
+namespace QGBA {
+
+class CoreController;
+
+class MemoryDump : public QDialog {
+Q_OBJECT
+
+public:
+	MemoryDump(std::shared_ptr<CoreController> controller, QWidget* parent = nullptr);
+
+public slots:
+	void setSegment(int);
+	void setAddress(uint32_t address);
+	void setByteCount(uint32_t);
+
+private slots:
+	void save();
+
+private:
+	QByteArray serialize();
+
+	Ui::MemoryDump m_ui;
+
+	std::shared_ptr<CoreController> m_controller;
+};
+
+}
diff --git a/src/platform/qt/MemoryDump.ui b/src/platform/qt/MemoryDump.ui
new file mode 100644
index 000000000..89915347e
--- /dev/null
+++ b/src/platform/qt/MemoryDump.ui
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MemoryDump</class>
+ <widget class="QDialog" name="MemoryDump">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>448</width>
+    <height>208</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Save Memory Range</string>
+  </property>
+  <layout class="QFormLayout" name="formLayout">
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Start Address:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QSpinBox" name="segment">
+       <property name="minimum">
+        <number>-1</number>
+       </property>
+       <property name="maximum">
+        <number>511</number>
+       </property>
+       <property name="displayIntegerBase">
+        <number>16</number>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLabel" name="label_3">
+       <property name="text">
+        <string>:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QSpinBox" name="address">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="accelerated">
+        <bool>true</bool>
+       </property>
+       <property name="prefix">
+        <string>0x</string>
+       </property>
+       <property name="maximum">
+        <number>268435455</number>
+       </property>
+       <property name="singleStep">
+        <number>16</number>
+       </property>
+       <property name="displayIntegerBase">
+        <number>16</number>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Byte Count:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QSpinBox" name="bytes">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="prefix">
+      <string>0x</string>
+     </property>
+     <property name="minimum">
+      <number>1</number>
+     </property>
+     <property name="maximum">
+      <number>268435456</number>
+     </property>
+     <property name="value">
+      <number>256</number>
+     </property>
+     <property name="displayIntegerBase">
+      <number>16</number>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0" colspan="2">
+    <widget class="QCheckBox" name="spanSegments">
+     <property name="text">
+      <string>Dump across banks</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0" colspan="2">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>MemoryDump</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>MemoryDump</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/src/platform/qt/MemoryView.cpp b/src/platform/qt/MemoryView.cpp
index 4d6827326..0aefb642f 100644
--- a/src/platform/qt/MemoryView.cpp
+++ b/src/platform/qt/MemoryView.cpp
@@ -7,6 +7,7 @@
 #include "MemoryView.h"
 
 #include "CoreController.h"
+#include "MemoryDump.h"
 
 #include <mgba/core/core.h>
 
@@ -44,6 +45,7 @@ MemoryView::MemoryView(std::shared_ptr<CoreController> controller, QWidget* pare
 	connect(m_ui.setAddress, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
 	        this, static_cast<void (MemoryView::*)(uint32_t)>(&MemoryView::jumpToAddress));
 	connect(m_ui.hexfield, &MemoryModel::selectionChanged, this, &MemoryView::updateSelection);
+	connect(m_ui.saveRange, &QAbstractButton::clicked, this, &MemoryView::saveRange);
 
 	connect(controller.get(), &CoreController::stopping, this, &QWidget::close);
 
@@ -69,6 +71,7 @@ void MemoryView::setIndex(int index) {
 	m_region = qMakePair(info.start, info.end);
 	m_ui.segments->setValue(-1);
 	m_ui.segments->setVisible(info.maxSegment > 0);
+	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);
 }
@@ -142,3 +145,12 @@ void MemoryView::updateStatus() {
 		break;
 	}
 }
+
+void MemoryView::saveRange() {
+	MemoryDump* memdump = new MemoryDump(m_controller);
+	memdump->setAttribute(Qt::WA_DeleteOnClose);
+	memdump->setAddress(m_selection.first);
+	memdump->setSegment(m_ui.segments->value());
+	memdump->setByteCount(m_selection.second - m_selection.first);
+	memdump->show();
+}
\ No newline at end of file
diff --git a/src/platform/qt/MemoryView.h b/src/platform/qt/MemoryView.h
index bc276af35..ac4bfe53d 100644
--- a/src/platform/qt/MemoryView.h
+++ b/src/platform/qt/MemoryView.h
@@ -28,6 +28,7 @@ private slots:
 	void setSegment(int);
 	void updateSelection(uint32_t start, uint32_t end);
 	void updateStatus();
+	void saveRange();
 
 private:
 	Ui::MemoryView m_ui;
diff --git a/src/platform/qt/MemoryView.ui b/src/platform/qt/MemoryView.ui
index b00c710f8..fb43fbe22 100644
--- a/src/platform/qt/MemoryView.ui
+++ b/src/platform/qt/MemoryView.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>822</width>
-    <height>886</height>
+    <width>874</width>
+    <height>900</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -52,6 +52,13 @@
        </property>
       </widget>
      </item>
+     <item>
+      <widget class="QLabel" name="segmentColon">
+       <property name="text">
+        <string>:</string>
+       </property>
+      </widget>
+     </item>
      <item>
       <widget class="QSpinBox" name="setAddress">
        <property name="accelerated">
@@ -271,6 +278,13 @@
        </property>
       </widget>
      </item>
+     <item>
+      <widget class="QPushButton" name="saveRange">
+       <property name="text">
+        <string>Save Range</string>
+       </property>
+      </widget>
+     </item>
      <item>
       <widget class="QPushButton" name="load">
        <property name="text">