diff --git a/.gitmodules b/.gitmodules index c884b29dd..462d67260 100644 --- a/.gitmodules +++ b/.gitmodules @@ -37,3 +37,6 @@ [submodule "third_party/LuaJIT"] path = third_party/LuaJIT url = https://github.com/Panda3DS-emu/LuaJIT +[submodule "third_party/mio"] + path = third_party/mio + url = https://github.com/vimpunk/mio diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c13d6346..80c70c1af 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 @@ -181,7 +182,7 @@ set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) if(ENABLE_QT_GUI) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp) - + source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) source_group("Header Files\\Qt" FILES ${FRONTEND_HEADER_FILES}) include_directories(${Qt6Gui_PRIVATE_INCLUDE_DIRS}) @@ -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( @@ -413,6 +414,12 @@ if(ENABLE_QT_GUI) target_link_libraries(Alber PRIVATE OpenGL::OpenGL OpenGL::EGL OpenGL::GLX) endif() endif() + + qt_add_resources(Alber "app_images" + PREFIX "/" + FILES + docs/img/rsob_icon.png + ) else() target_compile_definitions(Alber PUBLIC "PANDA3DS_FRONTEND_SDL=1") endif() diff --git a/docs/img/rsob_icon.png b/docs/img/rsob_icon.png new file mode 100644 index 000000000..4cabd3fb2 Binary files /dev/null and b/docs/img/rsob_icon.png differ diff --git a/include/emulator.hpp b/include/emulator.hpp index 4a5fab754..1901e4254 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 20213761a..114b1c1ef 100644 --- a/include/fs/romfs.hpp +++ b/include/fs/romfs.hpp @@ -18,5 +18,12 @@ namespace RomFS { std::vector> 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 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 7f0ff37f6..5e2ad1d85 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 codeFile; diff --git a/include/memory_mapped_file.hpp b/include/memory_mapped_file.hpp new file mode 100644 index 000000000..e83141555 --- /dev/null +++ b/include/memory_mapped_file.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#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 9f9f1014d..3bbabf35d 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -6,8 +6,9 @@ #include #include #include -#include #include +#include +#include #include "emulator.hpp" #include "panda_qt/screen.hpp" @@ -27,9 +28,11 @@ class MainWindow : public QMainWindow { std::thread emuThread; std::atomic appRunning = true; // Is the application itself running? - std::atomic needToLoadROM = false; + std::mutex messageQueueMutex; // Used for synchronizing messages between the emulator and UI std::filesystem::path romToLoad = ""; + bool needToLoadROM = false; + ScreenWidget screen; QComboBox* themeSelect = nullptr; QMenuBar* menuBar = nullptr; @@ -39,6 +42,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 d3d05839c..2546aa01f 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 5d87fccd2..43ecc149f 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -1,5 +1,7 @@ #include "emulator.hpp" + #include +#include #ifdef _WIN32 #include @@ -581,4 +583,64 @@ void Emulator::updateDiscord() { } #else void Emulator::updateDiscord() {} -#endif \ No newline at end of file +#endif + +static void dumpRomFSNode(const RomFS::RomFSNode& node, const char* romFSBase, const std::filesystem::path& path) { + for (auto& file : node.files) { + const auto p = path / file->name; + std::ofstream outFile(p); + + outFile.write(romFSBase + file->dataOffset, file->dataSize); + } + + for (auto& directory : node.directories) { + const auto newPath = path / directory->name; + + // Create the directory for the new folder + std::error_code ec; + std::filesystem::create_directories(newPath, ec); + + if (!ec) { + dumpRomFSNode(*directory, romFSBase, newPath); + } + } +} + +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 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 node = parseRomFSTree((uintptr_t)&romFS[0], size); + dumpRomFSNode(*node, (const char*)&romFS[0], path); + + 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 000000000..e62b46363 --- /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 b4887454f..2c2cc64f9 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -16,14 +16,20 @@ 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 pandaAction = fileMenu->addAction(tr("panda...")); - connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM); - 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 dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); + connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); + // Set up theme selection setTheme(Theme::Dark); themeSelect = new QComboBox(this); @@ -64,13 +70,17 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) void MainWindow::emuThreadMainLoop() { while (appRunning) { - if (needToLoadROM.load()) { - bool success = emu->loadROM(romToLoad); - if (!success) { - printf("Failed to load ROM"); - } + { + std::unique_lock lock(messageQueueMutex); + + if (needToLoadROM) { + needToLoadROM = false; - needToLoadROM.store(false, std::memory_order::seq_cst); + bool success = emu->loadROM(romToLoad); + if (!success) { + printf("Failed to load ROM"); + } + } } emu->runFrame(); @@ -93,17 +103,22 @@ void MainWindow::swapEmuBuffer() { void MainWindow::selectROM() { // Are we already waiting for a ROM to be loaded? Then complain about it! - if (needToLoadROM.load()) { - QMessageBox::warning(this, tr("Already loading ROM"), tr("Panda3DS is already busy loading a ROM, please wait")); - return; + { + std::unique_lock lock(messageQueueMutex); + if (needToLoadROM) { + QMessageBox::warning(this, tr("Already loading ROM"), tr("Panda3DS is already busy loading a ROM, please wait")); + return; + } } 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()) { + std::unique_lock lock(messageQueueMutex); + romToLoad = path.toStdU16String(); - needToLoadROM.store(true, std::memory_order_seq_cst); + needToLoadROM = true; } } @@ -175,4 +190,39 @@ void MainWindow::setTheme(Theme theme) { break; } } +} + +void MainWindow::dumpRomFS() { + 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()); + + // TODO: This might break if the game accesses RomFS while we're dumping, we should move it to the emulator thread when we've got a message queue going + messageQueueMutex.lock(); + RomFS::DumpingResult res = emu->dumpRomFS(path); + messageQueueMutex.unlock(); + + switch (res) { + case RomFS::DumpingResult::Success: break; // Yay! + case RomFS::DumpingResult::InvalidFormat: { + QMessageBox messageBox( + QMessageBox::Icon::Warning, tr("Invalid format for RomFS dumping"), + tr("The currently loaded app is not in a format that supports RomFS") + ); + + QAbstractButton* button = messageBox.addButton(tr("OK"), QMessageBox::ButtonRole::YesRole); + button->setIcon(QIcon(":/docs/img/rsob_icon.png")); + messageBox.exec(); + break; + } + + case RomFS::DumpingResult::NoRomFS: + QMessageBox::warning(this, tr("No RomFS found"), tr("No RomFS partition was found in the loaded app")); + break; + } } \ No newline at end of file diff --git a/third_party/mio b/third_party/mio new file mode 160000 index 000000000..8b6b7d878 --- /dev/null +++ b/third_party/mio @@ -0,0 +1 @@ +Subproject commit 8b6b7d878c89e81614d05edca7936de41ccdd2da