diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2c13d634..10dd66a5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,6 +53,7 @@ include_directories(third_party/xxhash/include)
 include_directories(third_party/httplib)
 include_directories(third_party/stb)
 include_directories(third_party/opengl)
+include_directories(third_party/mio/single_include)
 
 add_compile_definitions(NOMINMAX)             # Make windows.h not define min/max macros because third-party deps don't like it
 add_compile_definitions(WIN32_LEAN_AND_MEAN)  # Make windows.h not include literally everything
@@ -141,7 +142,7 @@ set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp
 				 src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp
 				 src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp
 				 src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp
-                 src/discord_rpc.cpp src/lua.cpp
+                 src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp
 )
 set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp)
 set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp
@@ -219,7 +220,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp
                  include/applets/applet.hpp include/applets/mii_selector.hpp include/math_util.hpp include/services/soc.hpp 
                  include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.hpp
                  include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp
-                 include/fs/archive_system_save_data.hpp include/lua_manager.hpp
+                 include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp
 )
 
 cmrc_add_resource_library(
diff --git a/include/emulator.hpp b/include/emulator.hpp
index 4a5fab75..1901e425 100644
--- a/include/emulator.hpp
+++ b/include/emulator.hpp
@@ -12,6 +12,7 @@
 #include "cpu.hpp"
 #include "crypto/aes_engine.hpp"
 #include "discord_rpc.hpp"
+#include "fs/romfs.hpp"
 #include "io_file.hpp"
 #include "lua_manager.hpp"
 #include "memory.hpp"
@@ -120,6 +121,7 @@ class Emulator {
 	void initGraphicsContext() { gpu.initGraphicsContext(window); }
 #endif
 
+	RomFS::DumpingResult dumpRomFS(const std::filesystem::path& path);
 	void setOutputSize(u32 width, u32 height) { gpu.setOutputSize(width, height); }
 
 	EmulatorConfig& getConfig() { return config; }
diff --git a/include/fs/romfs.hpp b/include/fs/romfs.hpp
index 20213761..114b1c1e 100644
--- a/include/fs/romfs.hpp
+++ b/include/fs/romfs.hpp
@@ -18,5 +18,12 @@ namespace RomFS {
 		std::vector<std::unique_ptr<RomFSNode>> files;
 	};
 
+	// Result codes when dumping RomFS. These are used by the frontend to print appropriate error messages if RomFS dumping fails
+	enum class DumpingResult {
+		Success = 0,
+		InvalidFormat = 1,  // ROM is a format that doesn't support RomFS, such as ELF
+		NoRomFS = 2
+	};
+
 	std::unique_ptr<RomFSNode> parseRomFSTree(uintptr_t romFS, u64 romFSSize);
 }  // namespace RomFS
\ No newline at end of file
diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp
index 7f0ff37f..5e2ad1d8 100644
--- a/include/loader/ncch.hpp
+++ b/include/loader/ncch.hpp
@@ -57,6 +57,7 @@ struct NCCH {
 	FSInfo exeFS;
 	FSInfo romFS;
 	CodeSetInfo text, data, rodata;
+	FSInfo partitionInfo;
 
 	// Contents of the .code file in the ExeFS
 	std::vector<u8> codeFile;
diff --git a/include/memory_mapped_file.hpp b/include/memory_mapped_file.hpp
new file mode 100644
index 00000000..e8314155
--- /dev/null
+++ b/include/memory_mapped_file.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <filesystem>
+#include <system_error>
+
+#include "helpers.hpp"
+#include "mio/mio.hpp"
+
+// Minimal RAII wrapper over memory mapped files
+
+class MemoryMappedFile {
+	std::filesystem::path filePath = "";  // path of our file
+	mio::mmap_sink map;                   // mmap sink for our file
+
+	u8* pointer = nullptr;  // Pointer to the contents of the memory mapped file
+	bool opened = false;
+
+  public:
+	bool exists() const { return opened; }
+	u8* data() const { return pointer; }
+
+	std::error_code flush();
+	MemoryMappedFile();
+	MemoryMappedFile(const std::filesystem::path& path);
+
+	~MemoryMappedFile();
+	// Returns true on success
+	bool open(const std::filesystem::path& path);
+	void close();
+
+	// TODO: For memory-mapped output files we'll need some more stuff such as a constructor that takes path/size/shouldCreate as parameters
+
+	u8& operator[](size_t index) { return pointer[index]; }
+	const u8& operator[](size_t index) const { return pointer[index]; }
+
+	auto begin() { return map.begin(); }
+	auto end() { return map.end(); }
+	auto cbegin() { return map.cbegin(); }
+	auto cend() { return map.cend(); }
+
+	mio::mmap_sink& getSink() { return map; }
+};
\ No newline at end of file
diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp
index 9f9f1014..e4a45487 100644
--- a/include/panda_qt/main_window.hpp
+++ b/include/panda_qt/main_window.hpp
@@ -39,6 +39,7 @@ class MainWindow : public QMainWindow {
 	void swapEmuBuffer();
 	void emuThreadMainLoop();
 	void selectROM();
+	void dumpRomFS();
 
 	// Tracks whether we are using an OpenGL-backed renderer or a Vulkan-backed renderer
 	bool usingGL = false;
diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp
index d3d05839..2546aa01 100644
--- a/src/core/loader/ncch.cpp
+++ b/src/core/loader/ncch.cpp
@@ -26,6 +26,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
 
 	codeFile.clear();
 	saveData.clear();
+	partitionInfo = info;
 
 	size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break
 	exheaderSize = *(u32*)&header[0x180];
diff --git a/src/emulator.cpp b/src/emulator.cpp
index 5d87fccd..e0de4a29 100644
--- a/src/emulator.cpp
+++ b/src/emulator.cpp
@@ -581,4 +581,63 @@ void Emulator::updateDiscord() {
 }
 #else
 void Emulator::updateDiscord() {}
-#endif
\ No newline at end of file
+#endif
+
+static void printNode(const RomFS::RomFSNode& node, int indentation) {
+	for (int i = 0; i < indentation; i++) {
+		printf("  ");
+	}
+	printf("%s/\n", std::string(node.name.begin(), node.name.end()).c_str());
+
+	for (auto& file : node.files) {
+		for (int i = 0; i <= indentation; i++) {
+			printf("  ");
+		}
+		printf("%s\n", std::string(file->name.begin(), file->name.end()).c_str());
+	}
+
+	indentation++;
+	for (auto& directory : node.directories) {
+		printNode(*directory, indentation);
+	}
+	indentation--;
+}
+
+RomFS::DumpingResult Emulator::dumpRomFS(const std::filesystem::path& path) {
+	using namespace RomFS;
+
+	if (romType != ROMType::NCSD && romType != ROMType::CXI && romType != ROMType::HB_3DSX) {
+		return DumpingResult::InvalidFormat;
+	}
+
+	// Contents of RomFS as raw bytes
+	std::vector<u8> romFS;
+	u64 size;
+
+	if (romType == ROMType::HB_3DSX) {
+		auto hb3dsx = memory.get3DSX();
+		if (!hb3dsx->hasRomFs()) {
+			return DumpingResult::NoRomFS;
+		}
+		size = hb3dsx->romFSSize;
+
+		romFS.resize(size);
+		hb3dsx->readRomFSBytes(&romFS[0], 0, size);
+	} else {
+		auto cxi = memory.getCXI();
+		if (!cxi->hasRomFS()) {
+			return DumpingResult::NoRomFS;
+		}
+
+		const u64 offset = cxi->romFS.offset;
+		size = cxi->romFS.size;
+
+		romFS.resize(size);
+		cxi->readFromFile(memory.CXIFile, cxi->partitionInfo, &romFS[0], offset - cxi->fileOffset, size);
+	}
+
+	std::unique_ptr<RomFSNode> node = parseRomFSTree((uintptr_t)&romFS[0], size);
+	printNode(*node, 0);
+
+	return DumpingResult::Success;
+}
\ No newline at end of file
diff --git a/src/memory_mapped_file.cpp b/src/memory_mapped_file.cpp
new file mode 100644
index 00000000..e62b4636
--- /dev/null
+++ b/src/memory_mapped_file.cpp
@@ -0,0 +1,37 @@
+#include "memory_mapped_file.hpp"
+
+MemoryMappedFile::MemoryMappedFile() : opened(false), filePath(""), pointer(nullptr) {}
+MemoryMappedFile::MemoryMappedFile(const std::filesystem::path& path) { open(path); }
+MemoryMappedFile::~MemoryMappedFile() { close(); }
+
+// TODO: This should probably also return the error one way or another eventually
+bool MemoryMappedFile::open(const std::filesystem::path& path) {
+	std::error_code error;
+	map = mio::make_mmap_sink(path.string(), 0, mio::map_entire_file, error);
+
+	if (error) {
+		opened = false;
+		return false;
+	}
+
+	filePath = path;
+	pointer = (u8*)map.data();
+	opened = true;
+	return true;
+}
+
+void MemoryMappedFile::close() {
+	if (opened) {
+		opened = false;
+		pointer = nullptr; // Set the pointer to nullptr to avoid errors related to lingering pointers
+
+		map.unmap();
+	}
+}
+
+std::error_code MemoryMappedFile::flush() {
+	std::error_code ret;
+	map.sync(ret);
+
+	return ret;
+}
\ No newline at end of file
diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp
index b4887454..977a7f80 100644
--- a/src/panda_qt/main_window.cpp
+++ b/src/panda_qt/main_window.cpp
@@ -16,13 +16,19 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
 	menuBar = new QMenuBar(this);
 	setMenuBar(menuBar);
 
+	// Create menu bar menus
 	auto fileMenu = menuBar->addMenu(tr("File"));
+	auto emulationMenu = menuBar->addMenu(tr("Emulation"));
+	auto toolsMenu = menuBar->addMenu(tr("Tools"));
+	auto helpMenu = menuBar->addMenu(tr("Help"));
+	auto aboutMenu = menuBar->addMenu(tr("About"));
+
+	// Create and bind actions for them
 	auto pandaAction = fileMenu->addAction(tr("panda..."));
 	connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM);
 
-	auto emulationMenu = menuBar->addMenu(tr("Emulation"));
-	auto helpMenu = menuBar->addMenu(tr("Help"));
-	auto aboutMenu = menuBar->addMenu(tr("About"));
+	auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS"));
+	connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS);
 
 	// Set up theme selection
 	setTheme(Theme::Dark);
@@ -71,6 +77,7 @@ void MainWindow::emuThreadMainLoop() {
 			}
 
 			needToLoadROM.store(false, std::memory_order::seq_cst);
+			emu->dumpRomFS("");
 		}
 
 		emu->runFrame();
@@ -98,8 +105,8 @@ void MainWindow::selectROM() {
 		return;
 	}
 
-	auto path =
-		QFileDialog::getOpenFileName(this, tr("Select 3DS ROM to load"), "", tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)"));
+	auto path = QFileDialog::getOpenFileName(
+		this, tr("Select 3DS ROM to load"), {}, tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)"), {});
 
 	if (!path.isEmpty()) {
 		romToLoad = path.toStdU16String();
@@ -175,4 +182,18 @@ void MainWindow::setTheme(Theme theme) {
 			break;
 		}
 	}
+}
+
+void MainWindow::dumpRomFS() {
+	// TODO: LOCK FILE MUTEX HERE
+	auto folder = QFileDialog::getExistingDirectory(
+		this, tr("Select folder to dump RomFS files to"), "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks
+	);
+
+	if (folder.isEmpty()) {
+		return;
+	}
+
+	std::filesystem::path path(folder.toStdU16String());
+	//RomFS::DumpingResult res = emu->dumpRomFS(path);
 }
\ No newline at end of file