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/Includes/menu.cpp b/Includes/menu.cpp index 27a212e..5d90eee 100644 --- a/Includes/menu.cpp +++ b/Includes/menu.cpp @@ -158,8 +158,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); + }); } } @@ -222,7 +223,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), @@ -287,8 +291,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() { @@ -349,9 +357,9 @@ Menu::Menu(const Config& config, Renderer& renderer) : } void Menu::render(Font& font) { - std::pair coordinates(100, startHeight); + std::pair coordinates(130, startHeight); std::string menutext = std::string(this->currentMenu->getLabel()); - font.draw(menutext, std::make_pair(300, 20)); + font.draw(menutext, std::make_pair(330, 20)); if (this->currentMenu->getChildNodes()->empty()) { font.draw("", coordinates); @@ -386,6 +394,25 @@ void Menu::render(Font& font) { rect.y = std::get<1>(coordinates); renderer.setDrawColor(0xFF, 0xFF, 0xFF, 0xFF); SDL_RenderDrawRect(renderer.getRenderer(), &rect); + + // TODO: Cache uncompressed icons w/ a limit on total size. + // Decide if we should only show the active one or all, position, etc... + XPR0Image const* icon = (*it)->getIcon(); + if (icon) { + SDL_Texture* texture = SDL_CreateTexture( + renderer.getRenderer(), SDL_PIXELFORMAT_BGRA8888, SDL_TEXTUREACCESS_STREAMING, + icon->width, icon->height); + uint8_t* textureData = nullptr; + int texturePitch = 0; + SDL_LockTexture(texture, nullptr, reinterpret_cast(&textureData), + &texturePitch); + icon->decompress(textureData); + SDL_UnlockTexture(texture); + + renderer.drawTexture(texture, 0, 10); + + SDL_DestroyTexture(texture); + } } coordinates = std::pair( diff --git a/Includes/menu.hpp b/Includes/menu.hpp index 0fb98b6..b4e4a24 100644 --- a/Includes/menu.hpp +++ b/Includes/menu.hpp @@ -8,6 +8,7 @@ #include "config.hpp" #include "font.h" #include "subApp.h" +#include "xbeInfo.h" #include "xbeScanner.h" class MenuNode; @@ -19,8 +20,10 @@ class MenuItem { MenuItem(MenuNode* parent, std::string const& label); virtual ~MenuItem(); virtual void execute(Menu*) = 0; + MenuNode* getParent() const; virtual std::string_view getLabel() const; + virtual XPR0Image const* getIcon() const { return nullptr; } void setParent(MenuNode* parent); @@ -76,14 +79,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. @@ -92,12 +93,16 @@ 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; + XPR0Image const* getIcon() const override { return ℑ } + protected: std::string path; + XPR0Image image; }; class MenuExec : public MenuItem { diff --git a/Includes/xbeInfo.cpp b/Includes/xbeInfo.cpp new file mode 100644 index 0000000..6d797d5 --- /dev/null +++ b/Includes/xbeInfo.cpp @@ -0,0 +1,45 @@ +#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("Failed to load save game icon from %s", path.c_str()); + return ret; + } + + if (!compressedImage.decompress(ret.imageData)) { + InfoLog::outputLine("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("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 1fdc92e..a4bde37 100644 --- a/Includes/xbeScanner.cpp +++ b/Includes/xbeScanner.cpp @@ -7,7 +7,10 @@ #define XBE_TYPE_MAGIC (0x48454258) #define SECTORSIZE 0x1000 -static bool scan(std::string const& path, std::vector& ret); +static std::pair getSaveImageFileOffset(FILE* file, + DWORD imageBase, + PXBE_SECTION_HEADER firstSectionHeader, + DWORD numberOfSections); XBEScanner* XBEScanner::singleton = nullptr; @@ -150,7 +153,44 @@ 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); -} \ No newline at end of file + results.emplace_back(xbeName, xbePath, saveImageInfo.first, saveImageInfo.second); +} + +// 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); +} diff --git a/Includes/xbeScanner.h b/Includes/xbeScanner.h index 96b7752..b57b798 100644 --- a/Includes/xbeScanner.h +++ b/Includes/xbeScanner.h @@ -9,6 +9,7 @@ #include #include #include +#include "xbeInfo.h" // TODO(#110): Reenable threading once hardware accelerated rendering is in place. // The current software-backed SDL approach causes the scanner thread to be starved, leading @@ -20,12 +21,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..b77bf3f --- /dev/null +++ b/Includes/xpr0Image.cpp @@ -0,0 +1,50 @@ +#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("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("Unexpected format %X (!=DXT1) in XPR0", header.resourceInfo.format); + return false; + } + + uint32_t dataSize = header.totalSize - header.headerSize; + if (dataSize > buffer.size()) { + InfoLog::outputLine("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..7e64290 --- /dev/null +++ b/Includes/xpr0Image.h @@ -0,0 +1,47 @@ +#ifndef NEVOLUTIONX_XPR0IMAGE_H +#define NEVOLUTIONX_XPR0IMAGE_H + +#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 92231b2..aa15c30 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,11 @@ SRCS += \ $(INCDIR)/timing.cpp \ $(INCDIR)/videoMenu.cpp \ $(INCDIR)/wipeCache.cpp \ + $(INCDIR)/xbeInfo.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