diff --git a/.gitmodules b/.gitmodules index 8365984..b92d972 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "3rdparty/NaturalSort"] path = 3rdparty/NaturalSort url = https://github.com/scopeInfinity/NaturalSort.git +[submodule "3rdparty/s3tc-dxt-decompression"] + path = 3rdparty/s3tc-dxt-decompression + url = https://github.com/Benjamin-Dobell/s3tc-dxt-decompression.git diff --git a/3rdparty/s3tc-dxt-decompression b/3rdparty/s3tc-dxt-decompression new file mode 160000 index 0000000..17074c2 --- /dev/null +++ b/3rdparty/s3tc-dxt-decompression @@ -0,0 +1 @@ +Subproject commit 17074c22c7857cfc4e843a07e99519a09c3a563f diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b2af1a..1959b23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,13 +35,16 @@ add_executable(${PROJECT_NAME} Includes/sntpClient.cpp Includes/subAppRouter.cpp Includes/subsystems.cpp - Includes/timing.cpp Includes/timeMenu.cpp + Includes/timing.cpp Includes/videoMenu.cpp Includes/wipeCache.cpp + Includes/xbeInfo.cpp Includes/xbeLauncher.cpp Includes/xbeScanner.cpp + Includes/xpr0Image.cpp 3rdparty/SDL_FontCache/SDL_FontCache.c + 3rdparty/s3tc-dxt-decompression/s3tc.cpp ) set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 17) diff --git a/Includes/menu.cpp b/Includes/menu.cpp index c12a115..c402167 100644 --- a/Includes/menu.cpp +++ b/Includes/menu.cpp @@ -157,8 +157,9 @@ MenuXbe::MenuXbe(MenuNode* parent, std::string const& label, std::string const& updateScanningLabel(); XBEScanner::scanPath( remainingScanPaths.front(), - [this](bool succeeded, std::list const& items, - long long duration) { this->onScanCompleted(succeeded, items, duration); }); + [this](bool succeeded, std::list const& items, long long duration) { + this->onScanCompleted(succeeded, items, duration); + }); } } @@ -221,7 +222,7 @@ void MenuXbe::updateScanningLabel() { } void MenuXbe::onScanCompleted(bool succeeded, - std::list const& items, + std::list const& items, long long duration) { (void)duration; std::string path = remainingScanPaths.front(); @@ -239,8 +240,9 @@ void MenuXbe::onScanCompleted(bool succeeded, updateScanningLabel(); XBEScanner::scanPath( remainingScanPaths.front(), - [this](bool succeeded, std::list const& items, - long long duration) { this->onScanCompleted(succeeded, items, duration); }); + [this](bool succeeded, std::list const& items, long long duration) { + this->onScanCompleted(succeeded, items, duration); + }); return; } @@ -251,7 +253,9 @@ void MenuXbe::createChildren() { std::vector> newChildren; for (auto& info: discoveredItems) { - newChildren.push_back(std::make_shared(info.name, info.path)); + XPR0Image saveIcon; + info.loadCompressedSaveGameIcon(saveIcon); + newChildren.push_back(std::make_shared(info.title, info.path, saveIcon)); } std::sort(begin(newChildren), end(newChildren), @@ -288,8 +292,12 @@ void MenuXbe::createChildren() { /****************************************************************************************** MenuLaunch ******************************************************************************************/ -MenuLaunch::MenuLaunch(std::string const& label, std::string const& path) : - MenuItem(label), path(path) { +MenuLaunch::MenuLaunch(std::string const& label, std::string path) : + MenuItem(label), path(std::move(path)), image() { +} + +MenuLaunch::MenuLaunch(std::string const& label, std::string path, XPR0Image image) : + MenuItem(label), path(std::move(path)), image(std::move(image)) { } MenuLaunch::~MenuLaunch() { diff --git a/Includes/menu.hpp b/Includes/menu.hpp index b746c51..e4ebde9 100644 --- a/Includes/menu.hpp +++ b/Includes/menu.hpp @@ -9,6 +9,7 @@ #include "config.hpp" #include "font.h" #include "subApp.h" +#include "xbeInfo.h" #include "xbeScanner.h" class MenuNode; @@ -77,14 +78,12 @@ class MenuXbe : public MenuNode { private: void superscroll(bool moveToPrevious); void updateScanningLabel(); - void onScanCompleted(bool succeeded, - std::list const& items, - long long duration); + void onScanCompleted(bool succeeded, std::list const& items, long long duration); void createChildren(); std::mutex childNodesLock; std::list remainingScanPaths; - std::vector discoveredItems; + std::vector discoveredItems; // Map of first letter to index of the first child in childNodes whose label starts with // that letter. @@ -93,12 +92,14 @@ class MenuXbe : public MenuNode { class MenuLaunch : public MenuItem { public: - MenuLaunch(std::string const& label, std::string const& path); + MenuLaunch(std::string const& label, std::string path); + MenuLaunch(std::string const& label, std::string path, XPR0Image image); ~MenuLaunch() override; void execute(Menu*) override; protected: std::string path; + XPR0Image image; }; class MenuExec : public MenuItem { diff --git a/Includes/sntpClient.cpp b/Includes/sntpClient.cpp index 0239596..9673aa9 100644 --- a/Includes/sntpClient.cpp +++ b/Includes/sntpClient.cpp @@ -74,7 +74,8 @@ void sntpClient::updateTime() const { message.originTimestamp.fractionalSeconds = htonl(message.originTimestamp.fractionalSeconds); if (message.originTimestamp.seconds || message.originTimestamp.fractionalSeconds) { - InfoLog::outputLine(InfoLog::INFO, "SNTP: Origin epoch is not 0: %lld\n", message.originTimestamp); + InfoLog::outputLine(InfoLog::INFO, "SNTP: Origin epoch is not 0: %lld\n", + message.originTimestamp); } message.transmitTimestamp.seconds = htonl(message.transmitTimestamp.seconds); @@ -95,7 +96,8 @@ void sntpClient::updateTime() const { } if (delta > allowedDriftSeconds) { - InfoLog::outputLine(InfoLog::DEBUG, "SNTP: Updating system clock (%llu seconds of drift)\n", delta); + InfoLog::outputLine(InfoLog::DEBUG, + "SNTP: Updating system clock (%llu seconds of drift)\n", delta); NTSTATUS status = NtSetSystemTime(&serverTime, nullptr); if (!NT_SUCCESS(status)) { InfoLog::outputLine(InfoLog::INFO, "SNTP: NtSetSystemTime failed: %X\n", status); diff --git a/Includes/xbeInfo.cpp b/Includes/xbeInfo.cpp new file mode 100644 index 0000000..e957b8d --- /dev/null +++ b/Includes/xbeInfo.cpp @@ -0,0 +1,48 @@ +#include "xbeInfo.h" +#include "infoLog.h" + +XBEInfo::Icon XBEInfo::loadSaveGameIcon() const { + Icon ret; + if (saveGameXPROffset <= 0 || saveGameXPRSize <= 0) { + return ret; + } + + XPR0Image compressedImage; + if (!loadCompressedSaveGameIcon(compressedImage)) { + InfoLog::outputLine(InfoLog::WARNING, "Failed to load save game icon from %s", + path.c_str()); + return ret; + } + + if (!compressedImage.decompress(ret.imageData)) { + InfoLog::outputLine(InfoLog::WARNING, "Failed to decompress save game icon from %s", + path.c_str()); + return ret; + } + + ret.width = compressedImage.width; + ret.height = compressedImage.height; + + return ret; +} + +bool XBEInfo::loadCompressedSaveGameIcon(XPR0Image& image) const { + image.clear(); + FILE* xbeFile = fopen(path.c_str(), "rb"); + if (!xbeFile) { + return false; + } + + fseek(xbeFile, saveGameXPROffset, SEEK_SET); + std::vector buffer(saveGameXPRSize); + size_t bytesRead = fread(buffer.data(), 1, saveGameXPRSize, xbeFile); + fclose(xbeFile); + + if (bytesRead != saveGameXPRSize) { + InfoLog::outputLine(InfoLog::WARNING, "Failed to read save game image from %s", + path.c_str()); + return false; + } + + return image.parse(buffer); +} diff --git a/Includes/xbeInfo.h b/Includes/xbeInfo.h new file mode 100644 index 0000000..c098fde --- /dev/null +++ b/Includes/xbeInfo.h @@ -0,0 +1,33 @@ +#ifndef NEVOLUTIONX_XBEINFO_H +#define NEVOLUTIONX_XBEINFO_H + +#include +#include +#include "xpr0Image.h" + +class XBEInfo { +public: + // TODO: See if the DXT1 compressed image can be used directly by the hardware instead. + struct Icon { + // imageData is always 32-bit color. + std::vector imageData; + uint32_t width{ 0 }; + uint32_t height{ 0 }; + }; + + XBEInfo(std::string xbeTitle, std::string xbePath, long xprOffset, size_t xprSize) : + title(std::move(xbeTitle)), path(std::move(xbePath)), saveGameXPROffset(xprOffset), + saveGameXPRSize(xprSize) {} + + Icon loadSaveGameIcon() const; + bool loadCompressedSaveGameIcon(XPR0Image& image) const; + + std::string title; + std::string path; + +private: + long saveGameXPROffset{ 0 }; + size_t saveGameXPRSize{ 0 }; +}; + +#endif // NEVOLUTIONX_XBEINFO_H diff --git a/Includes/xbeScanner.cpp b/Includes/xbeScanner.cpp index 3c45425..065f5a3 100644 --- a/Includes/xbeScanner.cpp +++ b/Includes/xbeScanner.cpp @@ -11,6 +11,13 @@ #define XBE_TYPE_MAGIC (0x48454258) #define SECTORSIZE 0x1000 +#ifdef NXDK +static std::pair getSaveImageFileOffset(FILE* file, + DWORD imageBase, + PXBE_SECTION_HEADER firstSectionHeader, + DWORD numberOfSections); +#endif + XBEScanner* XBEScanner::singleton = nullptr; XBEScanner* XBEScanner::getInstance() { @@ -165,8 +172,47 @@ void XBEScanner::QueueItem::processFile(const std::string& xbePath) { if (!strlen(xbeName)) { strncpy(xbeName, findData.cFileName, sizeof(xbeName) - 1); } + + auto firstSectionHeader = reinterpret_cast( + xbeData.data() + (DWORD)xbe->PointerToSectionTable - xbe->ImageBase); + std::pair saveImageInfo = getSaveImageFileOffset( + xbeFile, xbe->ImageBase, firstSectionHeader, xbe->NumberOfSections); + fclose(xbeFile); - results.emplace_back(xbeName, xbePath); + results.emplace_back(xbeName, xbePath, saveImageInfo.first, saveImageInfo.second); #endif // #ifdef NXDK } + +#ifdef NXDK +// Retrieves the FileAddress and FileSize members of the "$$XTIMAGE" section, which points +// to an XPR0 compressed icon for save games. +// +// NOTE: This will seek within the given file, if it is important to maintain the current +// read position it should be saved before calling this function. +static std::pair getSaveImageFileOffset(FILE* file, + DWORD imageBase, + PXBE_SECTION_HEADER firstSectionHeader, + DWORD numberOfSections) { + static const char SAVE_IMAGE_SECTION_NAME[] = "$$XTIMAGE"; + static const int SECTION_NAME_SIZE = sizeof(SAVE_IMAGE_SECTION_NAME); + + char nameBuffer[SECTION_NAME_SIZE] = { 0 }; + for (DWORD i = 0; i < numberOfSections; ++i) { + PXBE_SECTION_HEADER header = firstSectionHeader + i; + long nameOffset = reinterpret_cast(header->SectionName) - imageBase; + fseek(file, nameOffset, SEEK_SET); + size_t read_bytes = fread(nameBuffer, 1, SECTION_NAME_SIZE, file); + if (read_bytes != SECTION_NAME_SIZE) { + return std::make_pair(-1, -1); + } + + if (nameBuffer[SECTION_NAME_SIZE - 1] == 0 + && !strcmp(nameBuffer, SAVE_IMAGE_SECTION_NAME)) { + return std::make_pair(header->FileAddress, header->FileSize); + } + } + + return std::make_pair(-1, -1); +} +#endif // #ifdef NXDK diff --git a/Includes/xbeScanner.h b/Includes/xbeScanner.h index d33e68f..9c9088c 100644 --- a/Includes/xbeScanner.h +++ b/Includes/xbeScanner.h @@ -9,6 +9,7 @@ #include #include #include +#include "xbeInfo.h" #ifdef NXDK #include @@ -24,12 +25,6 @@ // direct subdirectories containing XBE files. class XBEScanner { public: - struct XBEInfo { - XBEInfo(std::string n, std::string p) : name(std::move(n)), path(std::move(p)) {} - std::string name; - std::string path; - }; - // (bool succeeded, std::list const& xbes, long long scanDuration) typedef std::function const&, long long)> Callback; diff --git a/Includes/xpr0Image.cpp b/Includes/xpr0Image.cpp new file mode 100644 index 0000000..9475ed1 --- /dev/null +++ b/Includes/xpr0Image.cpp @@ -0,0 +1,52 @@ +#include "xpr0Image.h" +#include "3rdparty/s3tc-dxt-decompression/s3tc.h" +#include "infoLog.h" + +static const uint32_t XPR0_MAGIC = 0x30525058; + +bool XPR0Image::parse(const std::vector& buffer) { + auto& header = *reinterpret_cast(buffer.data()); + if (header.magic != XPR0_MAGIC) { + InfoLog::outputLine(InfoLog::ERROR, "Unexpected magic bytes %X in XPR0", header.magic); + return false; + } + + static const uint32_t FORMAT_MASK = 0x0000FF00; + format = header.resourceInfo.format & FORMAT_MASK; + + static const uint32_t FORMAT_DXT1 = 0x00000C00; + // TODO: Investigate whether formats other than DXT1 are ever used. + if (format != FORMAT_DXT1) { + InfoLog::outputLine(InfoLog::ERROR, "Unexpected format %X (!=DXT1) in XPR0", + header.resourceInfo.format); + return false; + } + + uint32_t dataSize = header.totalSize - header.headerSize; + if (dataSize > buffer.size()) { + InfoLog::outputLine(InfoLog::ERROR, "Buffer size too small (%u < %u) in XPR0", + buffer.size(), dataSize); + } + + static const uint32_t UV_SIZE_MASK = 0x0FF00000; + static const uint32_t U_SHIFT = 20; + static const uint32_t V_SHIFT = 24; + const uint32_t sizeInfo = header.resourceInfo.format & UV_SIZE_MASK; + width = 1 << ((sizeInfo >> U_SHIFT) & 0x0F); + height = 1 << ((sizeInfo >> V_SHIFT) & 0x0F); + + auto imageDataStart = buffer.cbegin() + static_cast(header.headerSize); + imageData = std::vector(imageDataStart, buffer.cend()); + + return true; +} + +bool XPR0Image::decompress(std::vector& output) const { + output.resize(width * height * 4); + return decompress(output.data()); +} + +bool XPR0Image::decompress(uint8_t* output) const { + BlockDecompressImageDXT1(width, height, imageData.data(), (unsigned long*)output); + return true; +} diff --git a/Includes/xpr0Image.h b/Includes/xpr0Image.h new file mode 100644 index 0000000..6f8de03 --- /dev/null +++ b/Includes/xpr0Image.h @@ -0,0 +1,47 @@ +#ifndef NEVOLUTIONX_XPR0IMAGE_H +#define NEVOLUTIONX_XPR0IMAGE_H + +#include +#include + +// Encapsulates information about an XPR0 image. +class XPR0Image { +public: + struct ResourceInfo { + uint32_t common; + uint32_t data; + uint32_t lock; + uint32_t format; + uint32_t size; + }; + + struct XPRHeader { + uint32_t magic; + uint32_t totalSize; + uint32_t headerSize; + ResourceInfo resourceInfo; + uint32_t endOfHeader; // Should always == 0xFFFFFFFF + }; + + // Populates this XPR0Image from the given data buffer. + bool parse(std::vector const& buffer); + + // Copies 32bpp decompressed image data into the given `output` buffer. + // + // Returns true if the operation succeded, false if there was an error. + bool decompress(std::vector& output) const; + + bool decompress(uint8_t* output) const; + + void clear() { + width = height = format = 0; + imageData.clear(); + } + + uint32_t width{ 0 }; + uint32_t height{ 0 }; + uint32_t format; + std::vector imageData; +}; + +#endif // NEVOLUTIONX_XPR0IMAGE_H diff --git a/Makefile b/Makefile index 5576eed..5c29abe 100644 --- a/Makefile +++ b/Makefile @@ -25,9 +25,12 @@ SRCS += \ $(INCDIR)/timing.cpp \ $(INCDIR)/videoMenu.cpp \ $(INCDIR)/wipeCache.cpp \ + $(INCDIR)/xbeInfo.cpp \ $(INCDIR)/xbeLauncher.cpp \ $(INCDIR)/xbeScanner.cpp \ - $(CURDIR)/3rdparty/SDL_FontCache/SDL_FontCache.c + $(INCDIR)/xpr0Image.cpp \ + $(CURDIR)/3rdparty/SDL_FontCache/SDL_FontCache.c \ + $(CURDIR)/3rdparty/s3tc-dxt-decompression/s3tc.cpp NXDK_DIR ?= $(CURDIR)/../nxdk NXDK_SDL = y