From e7d2edd2034f152d5f01e01cd8aa59e077b14cea Mon Sep 17 00:00:00 2001 From: Nadia Holmquist Pedersen Date: Sat, 15 Apr 2023 21:51:34 +0200 Subject: [PATCH] Support loading Zstandard-compressed ROMs This is different from the archive support in that the compressed ROMs are standalone files, rather than archives, making it possible to use them exactly as if they were regular ROMs, while saving a bunch of space on disk. This is supported both for DS and GBA ROMs, though given GBA ROMs' generally small size it's mostly useful for the former. --- res/melon.plist.in | 6 +++ src/frontend/qt_sdl/CMakeLists.txt | 12 +++++ src/frontend/qt_sdl/ROMManager.cpp | 80 +++++++++++++++++++++++++++--- src/frontend/qt_sdl/main.cpp | 50 ++++++++++++++++--- 4 files changed, 133 insertions(+), 15 deletions(-) diff --git a/res/melon.plist.in b/res/melon.plist.in index 4c87eb97..3cfa3970 100644 --- a/res/melon.plist.in +++ b/res/melon.plist.in @@ -39,6 +39,10 @@ srl dsi ids + nds.zst + srl.zst + dsi.zst + ids.zst CFBundleTypeRole Viewer @@ -50,6 +54,8 @@ gba agb + gba.zst + agb.zst CFBundleTypeRole Viewer diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index 0387c58e..63bf53ee 100644 --- a/src/frontend/qt_sdl/CMakeLists.txt +++ b/src/frontend/qt_sdl/CMakeLists.txt @@ -83,6 +83,9 @@ pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2) pkg_check_modules(Slirp REQUIRED IMPORTED_TARGET slirp) pkg_check_modules(LibArchive REQUIRED IMPORTED_TARGET libarchive) +find_package(zstd CONFIG) +cmake_dependent_option(ENABLE_ZSTD "Enable support for Zstandard-compressed ROMs" ON "zstd_FOUND" OFF) + fix_interface_includes(PkgConfig::SDL2 PkgConfig::Slirp PkgConfig::LibArchive) add_compile_definitions(ARCHIVE_SUPPORT_ENABLED) @@ -157,6 +160,15 @@ target_link_libraries(melonDS PRIVATE core) target_link_libraries(melonDS PRIVATE PkgConfig::SDL2 PkgConfig::Slirp PkgConfig::LibArchive) target_link_libraries(melonDS PRIVATE ${QT_LINK_LIBS} ${CMAKE_DL_LIBS}) +if (ENABLE_ZSTD) + target_compile_definitions(melonDS PRIVATE ZSTD_ENABLED) + if (BUILD_STATIC) + target_link_libraries(melonDS PRIVATE zstd::libzstd_static) + else() + target_link_libraries(melonDS PRIVATE zstd::libzstd_shared) + endif() +endif() + if (UNIX) option(PORTABLE "Make a portable build that looks for its configuration in the current directory" OFF) elseif (WIN32) diff --git a/src/frontend/qt_sdl/ROMManager.cpp b/src/frontend/qt_sdl/ROMManager.cpp index ac525b74..967566a1 100644 --- a/src/frontend/qt_sdl/ROMManager.cpp +++ b/src/frontend/qt_sdl/ROMManager.cpp @@ -21,6 +21,7 @@ #include #include +#include #ifdef ARCHIVE_SUPPORT_ENABLED #include "ArchiveUtil.h" @@ -478,6 +479,27 @@ bool LoadBIOS() return true; } +u32 DecompressROM(const u8* inContent, const u32 inSize, u8** outContent) +{ + u64 realSize = ZSTD_getFrameContentSize(inContent, inSize); + + if (realSize == ZSTD_CONTENTSIZE_UNKNOWN || realSize == ZSTD_CONTENTSIZE_ERROR || realSize > 0x40000000) + { + return 0; + } + + u8* realContent = new u8[realSize]; + u64 decompressed = ZSTD_decompress(realContent, realSize, inContent, inSize); + + if (ZSTD_isError(decompressed)) + { + delete[] realContent; + return 0; + } + + *outContent = realContent; + return realSize; +} bool LoadROM(QStringList filepath, bool reset) { @@ -520,6 +542,27 @@ bool LoadROM(QStringList filepath, bool reset) fclose(f); filelen = (u32)len; +#if ZSTD_ENABLED + if (filename.length() > 4 && filename.substr(filename.length() - 4) == ".zst") + { + u8* outContent = nullptr; + u32 decompressed = DecompressROM(filedata, len, &outContent); + + if (decompressed > 0) + { + delete[] filedata; + filedata = outContent; + filelen = decompressed; + filename = filename.substr(0, filename.length() - 4); + } + else + { + delete[] filedata; + return false; + } + } +#endif + int pos = LastSep(filename); if(pos != -1) basepath = filename.substr(0, pos); @@ -530,14 +573,14 @@ bool LoadROM(QStringList filepath, bool reset) { // file inside archive - s32 lenread = Archive::ExtractFileFromArchive(filepath.at(0), filepath.at(1), &filedata, &filelen); - if (lenread < 0) return false; - if (!filedata) return false; - if (lenread != filelen) - { - delete[] filedata; - return false; - } + s32 lenread = Archive::ExtractFileFromArchive(filepath.at(0), filepath.at(1), &filedata, &filelen); + if (lenread < 0) return false; + if (!filedata) return false; + if (lenread != filelen) + { + delete[] filedata; + return false; + } std::string std_archivepath = filepath.at(0).toStdString(); basepath = std_archivepath.substr(0, LastSep(std_archivepath)); @@ -682,6 +725,27 @@ bool LoadGBAROM(QStringList filepath) fclose(f); filelen = (u32)len; +#if ZSTD_ENABLED + if (filename.length() > 4 && filename.substr(filename.length() - 4) == ".zst") + { + u8* outContent = nullptr; + u32 decompressed = DecompressROM(filedata, len, &outContent); + + if (decompressed > 0) + { + delete[] filedata; + filedata = outContent; + filelen = decompressed; + filename = filename.substr(0, filename.length() - 4); + } + else + { + delete[] filedata; + return false; + } + } +#endif + int pos = LastSep(filename); basepath = filename.substr(0, pos); romname = filename.substr(pos+1); diff --git a/src/frontend/qt_sdl/main.cpp b/src/frontend/qt_sdl/main.cpp index 7015c6da..b3ad0dc5 100644 --- a/src/frontend/qt_sdl/main.cpp +++ b/src/frontend/qt_sdl/main.cpp @@ -146,7 +146,7 @@ const QStringList ArchiveExtensions ".tar.lz", ".tar.lzma", ".tlz", ".tar.lrz", ".tlrz", - ".tar.lzo", ".tzo", + ".tar.lzo", ".tzo" #endif }; @@ -1568,9 +1568,26 @@ static bool SupportedArchiveByMimetype(const QMimeType& mimetype) return MimeTypeInList(mimetype, ArchiveMimeTypes); } +#ifdef ZSTD_ENABLED +static bool ZstdNdsRomByExtension(const QString& filename) +{ + if (filename.endsWith(".zst", Qt::CaseInsensitive)) + return NdsRomByExtension(filename.left(filename.size() - 4)); +} + +static bool ZstdGbaRomByExtension(const QString& filename) +{ + if (filename.endsWith(".zst", Qt::CaseInsensitive)) + return GbaRomByExtension(filename.left(filename.size() - 4)); +} +#endif static bool FileIsSupportedFiletype(const QString& filename, bool insideArchive = false) { +#ifdef ZSTD_ENABLED + if (ZstdNdsRomByExtension(filename) || ZstdGbaRomByExtension(filename)) + return true; +#endif if (NdsRomByExtension(filename) || GbaRomByExtension(filename) || SupportedArchiveByExtension(filename)) return true; @@ -2207,7 +2224,14 @@ void MainWindow::dropEvent(QDropEvent* event) const auto matchMode = romInsideArchive ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault; const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, matchMode); - if (NdsRomByExtension(filename) || NdsRomByMimetype(mimetype)) + bool isNdsRom = NdsRomByExtension(filename) || NdsRomByMimetype(mimetype); + bool isGbaRom = GbaRomByExtension(filename) || GbaRomByMimetype(mimetype); +#ifdef ZSTD_ENABLED + isNdsRom |= ZstdNdsRomByExtension(filename); + isGbaRom |= ZstdGbaRomByExtension(filename); +#endif + + if (isNdsRom) { if (!ROMManager::LoadROM(file, true)) { @@ -2227,7 +2251,7 @@ void MainWindow::dropEvent(QDropEvent* event) updateCartInserted(false); } - else if (GbaRomByExtension(filename) || GbaRomByMimetype(mimetype)) + else if (isGbaRom) { if (!ROMManager::LoadGBAROM(file)) { @@ -2452,14 +2476,26 @@ QStringList MainWindow::pickROM(bool gba) const QString console = gba ? "GBA" : "DS"; const QStringList& romexts = gba ? GbaRomExtensions : NdsRomExtensions; - static const QString filterSuffix = ArchiveExtensions.empty() - ? ");;Any file (*.*)" - : " *" + ArchiveExtensions.join(" *") + ");;Any file (*.*)"; + QString rawROMs = romexts.join(" *"); + QString extraFilters = ";;" + console + " ROMs (*" + rawROMs; + QString allROMs = rawROMs; + +#ifdef ZSTD_ENABLED + QString zstdROMs = "*" + romexts.join(".zst *") + ".zst"; + extraFilters += ");;Zstandard-compressed " + console + " ROMs (" + zstdROMs + ")"; + allROMs += " " + zstdROMs; +#endif +#ifdef ARCHIVE_SUPPORT_ENABLED + QString archives = "*" + ArchiveExtensions.join(" *"); + extraFilters += ";;Archives (" + archives + ")"; + allROMs += " " + archives; +#endif + extraFilters += ";;All files (*.*)"; const QString filename = QFileDialog::getOpenFileName( this, "Open " + console + " ROM", QString::fromStdString(Config::LastROMFolder), - console + " ROMs (*" + romexts.join(" *") + filterSuffix + "All supported files (*" + allROMs + ")" + extraFilters ); if (filename.isEmpty()) return {};