diff --git a/libs/JSystem/src/JKernel/JKRArchivePub.cpp b/libs/JSystem/src/JKernel/JKRArchivePub.cpp index 731a049457..b1ac093cc5 100644 --- a/libs/JSystem/src/JKernel/JKRArchivePub.cpp +++ b/libs/JSystem/src/JKernel/JKRArchivePub.cpp @@ -9,6 +9,24 @@ #include "JSystem/JKernel/JKRMemArchive.h" #include "JSystem/JUtility/JUTAssert.h" +#if DUSK_TPHD +#include "dusk/tphd/HdAssetLayer.hpp" + +namespace { +void register_copied_hd_bti( + JKRArchive* archive, JKRArchive::SDIFileEntry* fileEntry, void* buffer, u32 resourceSize) { + if (archive == NULL || fileEntry == NULL || buffer == NULL || resourceSize == 0 || + archive->mStringTable == NULL) + { + return; + } + + dusk::tphd::register_copied_hd_bti(archive->mEntryNum, + archive->mStringTable + fileEntry->getNameOffset(), buffer, resourceSize); +} +} // namespace +#endif + JKRArchive* JKRArchive::check_mount_already(s32 entryNum, JKRHeap* heap) { if (heap == NULL) { heap = JKRGetCurrentHeap(); @@ -196,6 +214,9 @@ u32 JKRArchive::readResource(void* buffer, u32 bufferSize, u32 type, const char* if (fileEntry) { u32 resourceSize; fetchResource(buffer, bufferSize, fileEntry, &resourceSize); +#if DUSK_TPHD + register_copied_hd_bti(this, fileEntry, buffer, resourceSize); +#endif return resourceSize; } @@ -214,6 +235,9 @@ u32 JKRArchive::readResource(void* buffer, u32 bufferSize, const char* path) { if (fileEntry) { u32 resourceSize; fetchResource(buffer, bufferSize, fileEntry, &resourceSize); +#if DUSK_TPHD + register_copied_hd_bti(this, fileEntry, buffer, resourceSize); +#endif return resourceSize; } @@ -226,6 +250,9 @@ u32 JKRArchive::readIdxResource(void* buffer, u32 bufferSize, u32 index) { if (fileEntry) { u32 resourceSize; fetchResource(buffer, bufferSize, fileEntry, &resourceSize); +#if DUSK_TPHD + register_copied_hd_bti(this, fileEntry, buffer, resourceSize); +#endif return resourceSize; } @@ -238,6 +265,9 @@ u32 JKRArchive::readResource(void* buffer, u32 bufferSize, u16 id) { if (fileEntry) { u32 resourceSize; fetchResource(buffer, bufferSize, fileEntry, &resourceSize); +#if DUSK_TPHD + register_copied_hd_bti(this, fileEntry, buffer, resourceSize); +#endif return resourceSize; } diff --git a/src/dusk/tphd/HdAssetLayer.cpp b/src/dusk/tphd/HdAssetLayer.cpp index 14fa6a1e2e..f506051d24 100644 --- a/src/dusk/tphd/HdAssetLayer.cpp +++ b/src/dusk/tphd/HdAssetLayer.cpp @@ -52,6 +52,11 @@ std::list>& g_textureBuffers() { return *p; } +std::unordered_map>& g_packCache() { + static auto* p = new std::unordered_map>{}; + return *p; +} + aurora::texture::ReplacementGroup& g_textureReplacementGroup() { static auto* p = new aurora::texture::ReplacementGroup{}; return *p; @@ -107,6 +112,11 @@ bool endsWithSuffix(std::string_view s, std::string_view suffix) { s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; } +bool path_exists(const std::filesystem::path& path) { + std::error_code ec; + return std::filesystem::is_regular_file(path, ec); +} + struct SDL_IODeleter { void operator()(SDL_IOStream* io) const { if (io != nullptr) { @@ -190,6 +200,32 @@ std::optional load_pack_from_file(const std::filesystem::path& path) { return TphdPack::loadFromMemory(*raw); } +std::shared_ptr load_pack_cached(const std::filesystem::path& path) { + const auto key = io::fs_path_to_string(path); + { + std::lock_guard lk{g_cacheMutex}; + const auto it = g_packCache().find(key); + if (it != g_packCache().end()) { + return it->second; + } + } + + auto loaded = load_pack_from_file(path); + if (!loaded) { + return {}; + } + + auto pack = std::make_shared(std::move(*loaded)); + { + std::lock_guard lk{g_cacheMutex}; + auto [it, inserted] = g_packCache().emplace(key, pack); + if (!inserted) { + return it->second; + } + } + return pack; +} + // Extract the path portion under "res/" from JSystem's absolute path. // Example: "/arcName/res/Stage/D_SB10/R00_00.arc" -> "res/Stage/D_SB10/R00_00.arc" std::string_view extractResPath(std::string_view gcPath) { @@ -402,7 +438,7 @@ DeswizzleResult deswizzleAllMips(const Gx2FormatMapping& m, const GtxSurface& s) void registerHdSurface(const Gx2FormatMapping& m, const GtxSurface& s, const void* pixelPtr, std::string_view gtxName, - u32 surfaceIdx) { + u32 surfaceIdx, bool replaceExistingPointer = false) { ZoneScoped; auto decoded = deswizzleAllMips(m, s); @@ -418,21 +454,62 @@ void registerHdSurface(const Gx2FormatMapping& m, const GtxSurface& s, std::lock_guard lk{g_cacheMutex}; g_textureBuffers().emplace_back(std::move(decoded.bytes)); const auto& bytes = g_textureBuffers().back(); - auto registration = aurora::texture::register_replacement( + const aurora::texture::RawTextureReplacement replacement{ + .bytes = {bytes.data(), bytes.size()}, + .width = s.width, + .height = s.height, + .mipCount = std::max(decoded.mipCount, 1u), + .gxFormat = m.newGxFormat, + .label = gtxName, + }; + + aurora::texture::ReplacementKey replacementKey{ aurora::texture::TexturePointerKey{.data = pixelPtr}, - aurora::texture::RawTextureReplacement{ - .bytes = {bytes.data(), bytes.size()}, - .width = s.width, - .height = s.height, - .mipCount = std::max(decoded.mipCount, 1u), - .gxFormat = m.newGxFormat, - .label = gtxName, - }); + }; + if (replaceExistingPointer) { + aurora::texture::unregister_replacements(replacementKey); + } + auto registration = aurora::texture::register_replacement(std::move(replacementKey), replacement); if (registration.id != 0) { g_textureReplacementGroup().registrations.push_back(std::move(registration)); } } +bool register_hd_bti_replacement_for_buffer(const TphdPack& pack, std::string_view resourceName, + void* buffer, size_t resourceSize, bool replaceExistingPointer) { + if (buffer == nullptr || resourceSize < 0x20 || !endsWithSuffixCI(resourceName, ".bti")) { + return false; + } + + const TmpkEntry* gtx = findGtxBySuffix(pack, resourceName); + if (!gtx) { + return false; + } + + auto surfaces = parseGtx(gtx->data); + if (surfaces.empty()) { + return false; + } + + const auto& s = surfaces[0]; + if (s.baseData.empty()) { + return false; + } + + const Gx2FormatMapping* m = findFormatMapping(s.format); + if (!m) { + return false; + } + + auto* timg = reinterpret_cast(buffer); + timg->imageOffset = 0x20; + const u8 hdMips = static_cast(std::clamp(s.mipCount, 1u, 11u)); + timg->mipmapCount = hdMips; + timg->maxLOD = static_cast((hdMips - 1) * 8); + registerHdSurface(*m, s, static_cast(buffer) + 0x20, gtx->name, 0, replaceExistingPointer); + return true; +} + // Lightweight RARC walker that returns per-file offsets without copying // arc bytes — we need absolute pointers into the cached HD arc bytes // (stable address) to match what the game later passes to GXInitTexObj. @@ -636,21 +713,42 @@ constexpr std::string_view kHdSkipList[] = { "res/Object/balloon2D.arc", "res/Object/Coach2D.arc", "res/Object/fileSel.arc", - "res/Layout/button.arc", - "res/Layout/Title2D.arc", - "res/Layout/main2D.arc", - "res/Layout/dmapres.arc", - "res/Layout/fmapres.arc", - "res/Layout/saveres.arc", - "res/Layout/fishres.arc", "res/FieldMap/res-f.arc", "res/FieldMap/res-d.arc", }; -bool is_skipped_path(std::string_view resPath) { - // Skip incompatible TPHD layout archives - if (resPath.starts_with("res/Layout/") || - resPath.starts_with("res/LayoutRevo/")) { +bool is_layout_arc_path(std::string_view resPath) { + return resPath.starts_with("res/Layout/") || + resPath.starts_with("res/LayoutRevo/"); +} + +std::filesystem::path hd_pack_path_for_arc(std::string_view resPath) { + std::filesystem::path packPath = g_contentPath / std::string(resPath); + packPath.replace_extension(".pack.gz"); + + if (!resPath.starts_with("res/Layout/")) { + return packPath; + } + + const std::filesystem::path arcPath{std::string(resPath)}; + std::string revoStem = arcPath.stem().string(); + if (!revoStem.empty() && revoStem.back() != 'R') { + revoStem += 'R'; + } + + const auto revoPackPath = g_contentPath / "res" / "LayoutRevo" / + (revoStem + ".pack.gz"); + if (path_exists(revoPackPath)) { + return revoPackPath; + } + + return packPath; +} + +bool should_skip_hd_arc_mount(std::string_view resPath) { + // Layout HD archives do not match the GC UI pipeline, but their pack.gz + // textures can still be registered against the vanilla archive. + if (is_layout_arc_path(resPath)) { return true; } for (auto skip : kHdSkipList) { @@ -659,6 +757,10 @@ bool is_skipped_path(std::string_view resPath) { return false; } +bool should_register_hd_pack_for_vanilla_arc(std::string_view resPath) { + return resPath.starts_with("res/Layout/"); +} + void* overlay_open(void* userData) { auto* entry = static_cast(userData); if (entry == nullptr) return nullptr; @@ -741,6 +843,7 @@ void rebuild_hd_overlay_locked() { ensure_overlay_callbacks_registered(); std::vector overlayFiles; + std::vector overlayEntries; for (std::filesystem::recursive_directory_iterator it(resRoot, ec), end; !ec && it != end; it.increment(ec)) { const bool regularFile = it->is_regular_file(ec); @@ -756,7 +859,29 @@ void rebuild_hd_overlay_locked() { const auto rel = arcPath.lexically_relative(g_contentPath); const std::string resPath = rel.generic_string(); - if (resPath.empty() || is_skipped_path(resPath)) continue; + if (resPath.empty()) continue; + + if (should_register_hd_pack_for_vanilla_arc(resPath)) { + auto packPath = hd_pack_path_for_arc(resPath); + if (path_exists(packPath)) { + auto& entry = g_overlayEntries().emplace_back(); + entry.dvdPath = "/" + resPath; + entry.arcPath = arcPath; + entry.packPath = std::move(packPath); + + const s32 entryNum = DVDConvertPathToEntrynum(entry.dvdPath.c_str()); + if (entryNum >= 0) { + g_entryNumToOverlay()[entryNum] = &entry; + HdLog.info("HD texture pack registered for vanilla arc: {} -> {}", + entry.dvdPath, entry.packPath.string()); + } else { + HdLog.warn("HD texture pack skipped because DVD path was not found: {}", + entry.dvdPath); + } + } + } + + if (should_skip_hd_arc_mount(resPath)) continue; const auto fileSize = get_file_size(arcPath); if (!fileSize.has_value()) { @@ -768,8 +893,7 @@ void rebuild_hd_overlay_locked() { auto& entry = g_overlayEntries().emplace_back(); entry.dvdPath = "/" + resPath; entry.arcPath = arcPath; - entry.packPath = arcPath; - entry.packPath.replace_extension(".pack.gz"); + entry.packPath = hd_pack_path_for_arc(resPath); entry.size = *fileSize; overlayFiles.push_back({ @@ -777,18 +901,19 @@ void rebuild_hd_overlay_locked() { .userData = &entry, .size = entry.size, }); + overlayEntries.push_back(&entry); } std::vector overlayEntryNums(overlayFiles.size(), -1); aurora_dvd_overlay_files(overlayFiles.data(), overlayFiles.size(), overlayEntryNums.data()); - auto entryIt = g_overlayEntries().begin(); - for (size_t i = 0; i < overlayEntryNums.size() && entryIt != g_overlayEntries().end(); ++i, ++entryIt) { + for (size_t i = 0; i < overlayEntryNums.size() && i < overlayEntries.size(); ++i) { + auto* entry = overlayEntries[i]; if (overlayEntryNums[i] < 0) { - HdLog.warn("HD overlay entry was not accepted by DVD FST: {}", entryIt->dvdPath); + HdLog.warn("HD overlay entry was not accepted by DVD FST: {}", entry->dvdPath); continue; } - g_entryNumToOverlay()[overlayEntryNums[i]] = &*entryIt; + g_entryNumToOverlay()[overlayEntryNums[i]] = entry; } HdLog.info("HD DVD overlay registered {} arcs from {}", @@ -802,6 +927,7 @@ void set_hd_content_path(std::filesystem::path contentPath) { std::lock_guard lk{g_cacheMutex}; clear_hd_texture_registrations_locked(); g_mountBuffers().clear(); + g_packCache().clear(); g_overlayEntries().clear(); g_entryNumToOverlay().clear(); g_arcRanges().clear(); @@ -816,7 +942,7 @@ std::optional*> try_load_hd_archive(std::string_view gcPath) { std::string_view resPath = extractResPath(gcPath); if (resPath.empty()) return std::nullopt; - if (is_skipped_path(resPath)) return std::nullopt; + if (should_skip_hd_arc_mount(resPath)) return std::nullopt; std::filesystem::path hdArcPath = g_contentPath / std::string(resPath); ZoneScoped; @@ -842,10 +968,8 @@ std::optional*> try_load_hd_archive(std::string_view gcPath) { hdBytesOpt->data(), hdBytesOpt->size())); // Sidecar pack.gz holds the HD textures. - auto hdPackPath = hdArcPath; - hdPackPath.replace_extension(".pack.gz"); - std::optional hdPack; - hdPack = load_pack_from_file(hdPackPath); + auto hdPackPath = hd_pack_path_for_arc(resPath); + auto hdPack = load_pack_cached(hdPackPath); // std::list keeps element addresses stable for aurora's pointer map. std::vector* mountBytes; @@ -861,7 +985,7 @@ std::optional*> try_load_hd_archive(std::string_view gcPath) { filename, static_cast(mountBytes->data()), mountBytes->size(), hdPack ? "yes" : "no"); - if (hdPack) { + if (hdPack != nullptr) { register_hd_textures_for_arc(*mountBytes, hdFiles, *hdPack, filename); } @@ -887,8 +1011,8 @@ void register_mounted_hd_archive(s32 entryNum, void* arcBytes, size_t arcSize) { register_hd_arc_range_locked(arcSpan.data(), arcSpan.size(), label); } - auto hdPack = load_pack_from_file(packPath); - if (!hdPack) { + auto hdPack = load_pack_cached(packPath); + if (hdPack == nullptr) { return; } @@ -896,6 +1020,31 @@ void register_mounted_hd_archive(s32 entryNum, void* arcBytes, size_t arcSize) { register_hd_textures_for_arc(arcSpan, hdFiles, *hdPack, label); } +void register_copied_hd_bti(s32 entryNum, std::string_view resourceName, void* buffer, + size_t resourceSize) { + if (entryNum < 0 || buffer == nullptr || resourceSize < 0x20 || + !endsWithSuffixCI(resourceName, ".bti")) { + return; + } + + std::filesystem::path packPath; + { + std::lock_guard lk{g_cacheMutex}; + auto it = g_entryNumToOverlay().find(entryNum); + if (it == g_entryNumToOverlay().end()) { + return; + } + packPath = it->second->packPath; + } + + auto hdPack = load_pack_cached(packPath); + if (hdPack == nullptr) { + return; + } + + register_hd_bti_replacement_for_buffer(*hdPack, resourceName, buffer, resourceSize, true); +} + std::optional find_registered_hd_archive_remaining(const void* ptr) { if (ptr == nullptr) return std::nullopt; diff --git a/src/dusk/tphd/HdAssetLayer.hpp b/src/dusk/tphd/HdAssetLayer.hpp index 11530fc745..e00cd572b3 100644 --- a/src/dusk/tphd/HdAssetLayer.hpp +++ b/src/dusk/tphd/HdAssetLayer.hpp @@ -25,6 +25,11 @@ std::optional*> try_load_hd_archive(std::string_view gcPath); // pointers the game will actually use. void register_mounted_hd_archive(s32 entryNum, void* arcBytes, size_t arcSize); +// Called after JKRArchive copies a BTI resource into caller-owned memory, such +// as item icons read out of an ARAM-mounted archive. +void register_copied_hd_bti(s32 entryNum, std::string_view resourceName, void* buffer, + size_t resourceSize); + // Returns bytes remaining in a registered HD archive range that contains ptr. // Used for debug heap accounting because some HD buffers are not JKR-owned. std::optional find_registered_hd_archive_remaining(const void* ptr);