#include "HdAssetLayer.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "JSystem/J3DGraphLoader/J3DModelLoader.h" #include "JSystem/JKernel/JKRArchive.h" #include "JSystem/JKernel/JKRDecomp.h" #include "JSystem/JUtility/JUTTexture.h" #include "dusk/endian.h" #include "dusk/io.hpp" #include "dusk/logging.h" #include "AddrLib.hpp" #include "GtxParser.hpp" #include "LosTable.hpp" #include "TphdPack.hpp" #include "tracy/Tracy.hpp" static aurora::Module HdLog("dusk::tphd::hd"); namespace dusk::tphd { namespace { std::filesystem::path g_contentPath; std::mutex g_cacheMutex; // Heap-allocated, never freed — these must outlive g_dComIfG_gameInfo's // static destructor which holds JKRArchives referencing these bytes. std::list>& g_mountBuffers() { static auto* p = new std::list>{}; return *p; } std::list>& g_textureBuffers() { static auto* p = new std::list>{}; 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; } struct HdArcRange { const void* begin = nullptr; size_t size = 0; std::string label; }; std::vector& g_arcRanges() { static auto* p = new std::vector{}; return *p; } struct HdOverlayEntry { std::string dvdPath; std::filesystem::path arcPath; std::filesystem::path packPath; size_t size = 0; }; std::list& g_overlayEntries() { static auto* p = new std::list{}; return *p; } std::unordered_map& g_entryNumToOverlay() { static auto* p = new std::unordered_map{}; return *p; } bool g_overlayCallbacksRegistered = false; void clear_hd_texture_registrations_locked() { aurora::texture::unregister_replacements(g_textureReplacementGroup()); g_textureReplacementGroup().registrations.clear(); g_textureBuffers().clear(); } void register_hd_arc_range_locked(const void* begin, size_t size, std::string_view label) { if (begin == nullptr || size == 0) return; g_arcRanges().push_back({ .begin = begin, .size = size, .label = std::string(label), }); } bool endsWithSuffix(std::string_view s, std::string_view suffix) { return s.size() >= suffix.size() && 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) { SDL_CloseIO(io); } } }; using IOStream = std::unique_ptr; IOStream open_stream(const std::filesystem::path& path) { const auto pathString = io::fs_path_to_string(path); return IOStream{SDL_IOFromFile(pathString.c_str(), "rb")}; } std::optional get_file_size(const std::filesystem::path& path) { auto stream = open_stream(path); if (stream == nullptr) { return std::nullopt; } const Sint64 size = SDL_GetIOSize(stream.get()); if (size < 0) { return std::nullopt; } return size; } // On-disk Yaz0 file header. struct Yaz0Header { /* 0x00 */ char magic[4]; // "Yaz0" /* 0x04 */ BE(u32) decompressedSize; /* 0x08 */ u8 pad[8]; }; static_assert(sizeof(Yaz0Header) == 0x10); // If `bytes` is a Yaz0 stream, return the inflated payload; otherwise nullopt. std::optional> tryDecodeYaz0(std::span bytes) { if (bytes.size() < sizeof(Yaz0Header) || std::memcmp(bytes.data(), "Yaz0", 4) != 0) { return std::nullopt; } ZoneScoped; const auto* hdr = reinterpret_cast(bytes.data()); const u32 expandedSize = hdr->decompressedSize; std::vector decoded(expandedSize); JKRDecomp::decodeSZS(const_cast(bytes.data()), decoded.data(), expandedSize, 0); return decoded; } std::optional> read_file(const std::filesystem::path& path) { auto stream = open_stream(path); if (stream == nullptr) { return std::nullopt; } const Sint64 len = SDL_GetIOSize(stream.get()); if (len < 0) { return std::nullopt; } std::vector buf(len); size_t total = 0; while (total < buf.size()) { const size_t got = SDL_ReadIO(stream.get(), buf.data() + total, buf.size() - total); if (got == 0) { break; } total += got; } if (total != buf.size() || SDL_GetIOStatus(stream.get()) == SDL_IO_STATUS_ERROR) { return std::nullopt; } return buf; } std::optional load_pack_from_file(const std::filesystem::path& path) { auto raw = read_file(path); if (!raw) { return std::nullopt; } 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) { auto p = gcPath.find("res/"); if (p == std::string_view::npos) return {}; return gcPath.substr(p); } // Case-insensitive ASCII suffix match — RARC archives lowercase filenames // at build time, but our HD pack.gz preserves the original Wii-U authoring // camelCase. Example: RARC has "coverbg.bti", pack has "coverBG.bti.gtx". bool endsWithSuffixCI(std::string_view s, std::string_view suffix) { if (s.size() < suffix.size()) return false; auto toLower = [](unsigned char c) -> unsigned char { return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; }; const char* a = s.data() + (s.size() - suffix.size()); for (size_t i = 0; i < suffix.size(); ++i) { if (toLower(a[i]) != toLower(suffix[i])) return false; } return true; } // Match an arc-relative path (e.g. "bmdr/model.bmd") against the Gfx2 entries // in the HD pack, which look like "tex/.../.gtx". const TmpkEntry* findGtxBySuffix(const TphdPack& pack, std::string_view arcRelPath) { const std::string tail = "/" + std::string(arcRelPath) + ".gtx"; for (const auto& e : pack.entries()) { if (e.data.size() < 4 || std::memcmp(e.data.data(), "Gfx2", 4) != 0) continue; if (endsWithSuffixCI(e.name, tail)) return &e; } return nullptr; } // Post-deswizzle CPU expansions to RGBA8. Used for formats whose HD layout // can't be directly sampled with a GPU view swizzle (IA4 nibble unpack, // RGB565 16-bit), and as a fallback if R8_PC/RG8_PC view swizzle isn't // available. GC sampling semantics: I8 -> (I,I,I,I); IA4/IA8 -> (I,I,I,A). std::vector expandR5G6B5toRgba8(std::span in, u32 width, u32 height) { std::vector out(static_cast(width) * height * 4); const size_t pixelCount = static_cast(width) * height; for (size_t i = 0; i < pixelCount && (i * 2 + 1) < in.size(); ++i) { // GX2 stores RGB565 pixel data in GPU-native LE u16 px; std::memcpy(&px, &in[i * 2], sizeof(px)); u8 b5 = static_cast((px >> 11) & 0x1F); u8 g6 = static_cast((px >> 5) & 0x3F); u8 r5 = static_cast(px & 0x1F); out[i * 4 + 0] = static_cast((r5 << 3) | (r5 >> 2)); out[i * 4 + 1] = static_cast((g6 << 2) | (g6 >> 4)); out[i * 4 + 2] = static_cast((b5 << 3) | (b5 >> 2)); out[i * 4 + 3] = 0xFF; } return out; } // IA4: high nibble = A, low nibble = I (matches aurora's GC IA4 decoder). std::vector expandIA4toRgba8(std::span in, u32 width, u32 height) { std::vector out(static_cast(width) * height * 4); const size_t pixelCount = static_cast(width) * height; for (size_t i = 0; i < pixelCount && i < in.size(); ++i) { u8 b = in[i]; u8 A = static_cast((b & 0xF0) | (b >> 4)); u8 I = static_cast(((b & 0x0F) << 4) | (b & 0x0F)); out[i * 4 + 0] = I; out[i * 4 + 1] = I; out[i * 4 + 2] = I; out[i * 4 + 3] = A; } return out; } enum class Expansion { None, R5G6B5_to_RGBA8, IA4_to_RGBA8, }; struct Gx2FormatMapping { u32 gx2Format; // GX2 surface format u8 newGxFormat; // Aurora PC-target format u32 bpp; // Deswizzle bits-per-pixel (per pixel, or per 4x4 block for BCn) bool isBcn; Expansion expansion; // Optional post-deswizzle CPU expansion }; // I8/IA8 pass through as R8_PC/RG8_PC (aurora applies .rrrr/.rrrg view // swizzle on the GPU side — half / quarter the VRAM of CPU-expanded RGBA8). // IA4 + RGB565 need CPU expansion (nibble / 16-bit unpack). CMPR stays // BC1_PC (compressed on the GPU). constexpr Gx2FormatMapping kFormatMap[] = { // gx2 fmt PC target bpp isBcn expansion { 0x01 /* I8 */, 0x41 /* R8_PC */, 8, false, Expansion::None }, { 0x02 /* IA4 */, 0x46 /* RGBA8_PC */, 8, false, Expansion::IA4_to_RGBA8 }, { 0x07 /* IA8 */, 0x43 /* RG8_PC */, 16, false, Expansion::None }, { 0x08 /* RGB565 */, 0x46 /* RGBA8_PC */, 16, false, Expansion::R5G6B5_to_RGBA8 }, { 0x1A /* RGBA8 */, 0x46 /* RGBA8_PC */, 32, false, Expansion::None }, { 0x31 /* CMPR */, 0x4E /* BC1_PC */, 64, true, Expansion::None }, }; const Gx2FormatMapping* findFormatMapping(u32 gx2Format) { for (const auto& m : kFormatMap) { if (m.gx2Format == gx2Format) return &m; } return nullptr; } std::vector applyExpansion(Expansion exp, std::vector linear, u32 w, u32 h) { switch (exp) { case Expansion::R5G6B5_to_RGBA8: return expandR5G6B5toRgba8(linear, w, h); case Expansion::IA4_to_RGBA8: return expandIA4toRgba8(linear, w, h); case Expansion::None: break; } return linear; } // Per-mip tile-mode + pitch. Demote rule mirrored from decaf-emu's // R600AddrLib::ComputeSurfaceMipLevelTileMode (MIT, AMD-derived) — see // AddrLib.cpp header for the full copyright notice. // // R700 macro-tile size: 32 × 16 elements (BCN element = 4×4 block). // Mips below that are demoted to Tiled1DThin1 (microtile-only, 8-element // align). struct MipLevelDesc { u32 width; u32 height; u32 pitch; addrlib::TileMode tileMode; }; MipLevelDesc mipLevelDesc(const GtxSurface& s, u32 level, bool isBcn, u32 bpp) { MipLevelDesc d{}; d.width = std::max(1u, s.width >> level); d.height = std::max(1u, s.height >> level); if (level == 0) { d.pitch = s.pitch; d.tileMode = static_cast(s.tileMode); return d; } const addrlib::SurfaceInfoIn si{ .width = s.width, .height = s.height, .bpp = bpp, .mipLevel = level, .tileMode = static_cast(s.tileMode), .isBcn = isBcn, }; addrlib::SurfaceInfoOut so{}; addrlib::computeSurfaceInfo(si, so); d.pitch = so.pitch; // block units for BCN, pixel units for plain. d.tileMode = so.tileMode; return d; } // Slice the bytes for a single mip level. Wii-U quirk: mipOffsets[0] is // often image_size, not a mipData offset. Level 1 // always starts at 0 in mipData; level >= 2 uses mipOffsets[level - 1]. std::span mipLevelData(const GtxSurface& s, u32 level) { if (level == 0) return s.baseData; if (level >= s.mipCount) return {}; u32 start = 0; if (level >= 2 && level - 1 < s.mipOffsets.size()) { start = s.mipOffsets[level - 1]; } if (start >= s.mipData.size()) return {}; u32 end = static_cast(s.mipData.size()); if (level + 1 < s.mipCount && level < s.mipOffsets.size()) { const u32 next = s.mipOffsets[level]; if (next > start && next <= s.mipData.size()) end = next; } return s.mipData.subspan(start, end - start); } struct DeswizzleResult { std::vector bytes; u32 mipCount; }; DeswizzleResult deswizzleAllMips(const Gx2FormatMapping& m, const GtxSurface& s) { ZoneScoped; DeswizzleResult out{}; const u32 maxLevels = std::min(s.mipCount, 13u); for (u32 level = 0; level < maxLevels; ++level) { const std::span slice = mipLevelData(s, level); if (slice.empty()) break; const MipLevelDesc lvl = mipLevelDesc(s, level, m.isBcn, m.bpp); const addrlib::SurfaceDesc desc{ .width = lvl.width, .height = lvl.height, .pitch = lvl.pitch, .bpp = m.bpp, .tileMode = lvl.tileMode, .swizzle = s.swizzle, .isBcn = m.isBcn, .isDepth = false, }; auto linear = applyExpansion(m.expansion, addrlib::deswizzle(desc, slice), lvl.width, lvl.height); out.bytes.insert(out.bytes.end(), linear.begin(), linear.end()); out.mipCount = level + 1; } return out; } void registerHdSurface(const Gx2FormatMapping& m, const GtxSurface& s, const void* pixelPtr, std::string_view gtxName, u32 surfaceIdx, bool replaceExistingPointer = false) { ZoneScoped; auto decoded = deswizzleAllMips(m, s); HdLog.info("HD reg: ptr={} fmt=0x{:02X} {}x{} mips={}/{} bytes={} gtx={}[{}]", pixelPtr, m.newGxFormat, s.width, s.height, decoded.mipCount, s.mipCount, decoded.bytes.size(), gtxName, surfaceIdx); if (decoded.bytes.empty() || pixelPtr == nullptr) { return; } std::lock_guard lk{g_cacheMutex}; g_textureBuffers().emplace_back(std::move(decoded.bytes)); const auto& bytes = g_textureBuffers().back(); 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}, }; 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; } // Absolute offset of slot `slotIdx`'s BTI header within a BMD's TEX1 block. // Returns 0 on failure (the TEX1 table never sits at offset 0, so 0 is a // safe sentinel). u32 bmdSlotBtiOffset(std::span bmd, u32 slotIdx) { constexpr size_t kBlocksOffset = offsetof(J3DModelFileData, mBlocks); // = 0x20 if (bmd.size() < kBlocksOffset || std::memcmp(bmd.data(), "J3D2", 4) != 0) return 0; const auto* fileData = reinterpret_cast(bmd.data()); const u32 numSections = fileData->mBlockNum; size_t pos = kBlocksOffset; for (u32 i = 0; i < numSections && pos + sizeof(J3DModelBlock) <= bmd.size(); ++i) { const auto* blk = reinterpret_cast(bmd.data() + pos); const u32 blockSize = blk->mBlockSize; if (blk->mBlockType == 'TEX1') { const auto* tex1 = reinterpret_cast(bmd.data() + pos); const u16 numTex = tex1->mTextureNum; if (slotIdx >= numTex) return 0; const size_t btiAbs = pos + static_cast(tex1->mpTextureRes) + slotIdx * 0x20; if (btiAbs + 0x20 > bmd.size()) return 0; return static_cast(btiAbs); } if (blockSize == 0) break; pos += blockSize; } return 0; } size_t register_hd_bmd_textures_for_buffer(const TphdPack& pack, std::string_view resourceName, void* buffer, size_t resourceSize, bool replaceExistingPointer) { if (buffer == nullptr || resourceSize < 0x20) return 0; if (!endsWithSuffixCI(resourceName, ".bmd") && !endsWithSuffixCI(resourceName, ".bdl")) return 0; const TmpkEntry* gtx = findGtxBySuffix(pack, resourceName); if (gtx == nullptr) return 0; std::span bmdBytes(static_cast(buffer), resourceSize); auto surfaces = parseGtx(gtx->data); size_t reg = 0; for (u32 i = 0; i < surfaces.size(); ++i) { const auto& s = surfaces[i]; if (s.baseData.empty()) continue; const Gx2FormatMapping* m = findFormatMapping(s.format); if (!m) continue; // HD-stub BMDs collapse every BTI's imageOffset to the same // pixel address. Rewrite each to be slot-unique so our pointer // map doesn't overwrite. const u32 btiAbs = bmdSlotBtiOffset(bmdBytes, i); if (btiAbs == 0) continue; auto* timg = reinterpret_cast(bmdBytes.data() + btiAbs); if (timg->imageOffset == 0) { HdLog.debug("Skip cross-arc placeholder slot {} in {}: " "imageOffset==0", i, gtx->name); continue; } const u32 newImgOff = 0x20 + i * 0x20; timg->imageOffset = static_cast(newImgOff); const u8 hdMips = static_cast(std::clamp(s.mipCount, 1u, 11u)); timg->mipmapCount = hdMips; timg->maxLOD = static_cast((hdMips - 1) * 8); timg->maxAnisotropy = GX_ANISO_4; registerHdSurface(*m, s, bmdBytes.data() + btiAbs + newImgOff, gtx->name, i, replaceExistingPointer); ++reg; } return reg; } // 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. struct ArcFileInfo { std::string path; // e.g. "bmdr/model.bmd" u32 dataOffset; // absolute offset from arc base u32 dataSize; }; std::vector parseRarcFiles(std::span arc) { std::vector out; if (arc.size() < 0x40 || std::memcmp(arc.data(), "RARC", 4) != 0) return out; constexpr size_t kMetaBase = sizeof(SArcHeader); // = 0x20 if (arc.size() < kMetaBase + sizeof(SArcDataInfo)) return out; const auto* hdr = reinterpret_cast(arc.data()); const auto* dataInfo = reinterpret_cast(arc.data() + kMetaBase); const u32 nodeCount = dataInfo->num_nodes; const size_t nodeTbl = dataInfo->node_offset + kMetaBase; const size_t fileTbl = dataInfo->file_entry_offset + kMetaBase; const size_t strTbl = dataInfo->string_table_offset + kMetaBase; const size_t dataBase = kMetaBase + hdr->file_data_offset; auto readStringAt = [&](u32 offset) -> std::string { const u8* start = arc.data() + strTbl + offset; const u8* bufferEnd = arc.data() + arc.size(); if (start >= bufferEnd) return {}; const void* nul = std::memchr(start, 0, static_cast(bufferEnd - start)); const u8* terminator = nul ? static_cast(nul) : bufferEnd; return std::string(reinterpret_cast(start), static_cast(terminator - start)); }; const auto* nodes = reinterpret_cast( arc.data() + nodeTbl); const auto* files = reinterpret_cast( arc.data() + fileTbl); for (u32 ni = 0; ni < nodeCount; ++ni) { const auto& node = nodes[ni]; const std::string dirName = readStringAt(node.name_offset); const u16 fc = node.num_entries; const u32 firstIdx = node.first_file_index; const bool isRoot = (ni == 0); for (u32 fi = 0; fi < fc; ++fi) { const auto& entry = files[firstIdx + fi]; const u32 typeFlagsAndName = entry.type_flags_and_name_offset; const u8 typeFlags = static_cast(typeFlagsAndName >> 24); // Bit 0x01 = file, 0x02 = directory. We only want files. if ((typeFlags & 0x03) != 0x01) continue; std::string fname = readStringAt(typeFlagsAndName & 0xFFFFFF); if (fname == "." || fname == "..") continue; out.push_back({ (!isRoot && !dirName.empty()) ? dirName + "/" + fname : std::move(fname), static_cast(dataBase + entry.data_offset), entry.data_size, }); } } return out; } // Walk the HD arc, pair BMDs with their pack.gz GTX entries, deswizzle each // HD surface, and register the decoded bytes with aurora under the absolute // pointer that GXInitTexObj will later receive. // // arcBytes must point at the mounted archive bytes the game will later use; // aurora's pointer lookups depend on those addresses staying valid. void register_hd_textures_for_arc(std::span arcBytes, const std::vector& files, const TphdPack& pack, std::string_view arcLabel) { ZoneScoped; ZoneText(arcLabel.data(), arcLabel.size()); size_t bmdReg = 0; size_t btiReg = 0; // Phase A: per-slot textures inside BMD/BDL models. for (const auto& f : files) { bmdReg += register_hd_bmd_textures_for_buffer(pack, f.path, arcBytes.data() + f.dataOffset, f.dataSize, false); } // Phase B: standalone .bti files. Each BTI is its own arc entry; the // game loads it via JUTTexture (or similar) which calls GXInitTexObj // with `(u8*)resTIMG + imageOffset`. Register that exact pointer. for (const auto& f : files) { if (register_hd_bti_replacement_for_buffer(pack, f.path, arcBytes.data() + f.dataOffset, f.dataSize, false)) { ++btiReg; } } HdLog.info("registerHdTextures[{}]: {} BMD-slot, {} standalone-BTI replacements", arcLabel, bmdReg, btiReg); } // HD arcs whose Wii-U layouts don't match the GC UI pipeline. constexpr std::string_view kHdSkipList[] = { "res/Object/fileSel.arc", }; 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) { if (resPath == skip) return true; } 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; SDL_IOStream* stream = open_stream(entry->arcPath).release(); if (stream == nullptr) { HdLog.warn("HD overlay open failed: {} ({})", entry->arcPath.string(), SDL_GetError()); return nullptr; } return stream; } void overlay_close(void* handle) { auto* stream = static_cast(handle); if (stream == nullptr) return; SDL_CloseIO(stream); } int64_t overlay_read(void* handle, uint8_t* buf, size_t len) { auto* stream = static_cast(handle); if (stream == nullptr || buf == nullptr) { return -1; } if (len == 0) { return 0; } const size_t got = SDL_ReadIO(stream, buf, len); if (got == 0 && SDL_GetIOStatus(stream) == SDL_IO_STATUS_ERROR) { return -1; } return static_cast(got); } int64_t overlay_seek(void* handle, int64_t offset, int32_t whence) { auto* stream = static_cast(handle); if (stream == nullptr) { return -1; } const Sint64 pos = SDL_SeekIO(stream, offset, static_cast(whence)); return pos < 0 ? -1 : static_cast(pos); } void ensure_overlay_callbacks_registered() { if (g_overlayCallbacksRegistered) { return; } static constexpr AuroraOverlayCallbacks callbacks{ .open = overlay_open, .close = overlay_close, .read = overlay_read, .seek = overlay_seek, }; aurora_dvd_overlay_callbacks(&callbacks); g_overlayCallbacksRegistered = true; } void rebuild_hd_overlay_locked() { if (g_contentPath.empty()) { if (g_overlayCallbacksRegistered) { aurora_dvd_overlay_files(nullptr, 0, nullptr); } return; } std::error_code ec; const auto resRoot = g_contentPath; if (!std::filesystem::is_directory(resRoot, ec)) { HdLog.warn("HD content path has no res directory: {}", g_contentPath.string()); return; } const s32 baseEntryCount = aurora_dvd_base_entry_count(); if (baseEntryCount <= 0) { HdLog.warn("DVD overlay skipped because no base DVD FST is loaded yet"); return; } 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); if (ec) { ec.clear(); continue; } if (!regularFile) continue; const auto& arcPath = it->path(); const auto rel = arcPath.lexically_relative(g_contentPath); const std::string resPath = rel.generic_string(); 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()) { HdLog.warn("HD overlay file size failed: {} ({})", arcPath.string(), SDL_GetError()); continue; } auto& entry = g_overlayEntries().emplace_back(); entry.dvdPath = "/" + resPath; entry.arcPath = arcPath; entry.packPath = hd_pack_path_for_arc(resPath); entry.size = *fileSize; overlayFiles.push_back({ .fileName = entry.dvdPath.c_str(), .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()); 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: {}", entry->dvdPath); continue; } g_entryNumToOverlay()[overlayEntryNums[i]] = entry; } HdLog.info("HD DVD overlay registered {} files (arcs, .jpc and Audiores) from {}", overlayFiles.size(), g_contentPath.string()); } } void set_hd_content_path(std::filesystem::path contentPath) { g_contentPath = std::move(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(); rebuild_hd_overlay_locked(); load_los_table(g_contentPath); HdLog.info("HD content path set to: {}", g_contentPath.empty() ? "(disabled)" : g_contentPath.string()); } std::optional*> try_load_hd_archive(std::string_view gcPath) { if (g_contentPath.empty()) return std::nullopt; std::string_view resPath = extractResPath(gcPath); if (resPath.empty()) return std::nullopt; if (should_skip_hd_arc_mount(resPath)) return std::nullopt; std::filesystem::path hdArcPath = g_contentPath / std::string(resPath); ZoneScoped; #ifdef TRACY_ENABLE { auto fn = hdArcPath.filename().string(); ZoneText(fn.c_str(), fn.size()); } #endif auto hdBytesOpt = read_file(hdArcPath); if (!hdBytesOpt) { return std::nullopt; } if (auto inflated = tryDecodeYaz0(*hdBytesOpt)) { HdLog.info("HD arc Yaz0-decompressed: {} -> {} bytes", hdArcPath.filename().string(), inflated->size()); hdBytesOpt = std::move(inflated); } auto hdFiles = parseRarcFiles(std::span( hdBytesOpt->data(), hdBytesOpt->size())); // Sidecar pack.gz holds the HD textures. 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; std::string filename = hdArcPath.filename().string(); { std::lock_guard lk{g_cacheMutex}; g_mountBuffers().emplace_back(std::move(*hdBytesOpt)); mountBytes = &g_mountBuffers().back(); register_hd_arc_range_locked(mountBytes->data(), mountBytes->size(), filename); } HdLog.info("HD arc mount buffer allocated: {} at {} ({} bytes, pack.gz={})", filename, static_cast(mountBytes->data()), mountBytes->size(), hdPack ? "yes" : "no"); if (hdPack != nullptr) { register_hd_textures_for_arc(*mountBytes, hdFiles, *hdPack, filename); } return mountBytes; } void register_mounted_hd_archive(s32 entryNum, void* arcBytes, size_t arcSize) { if (entryNum < 0 || arcBytes == nullptr || arcSize == 0) return; std::filesystem::path packPath; std::string label; { std::lock_guard lk{g_cacheMutex}; auto it = g_entryNumToOverlay().find(entryNum); if (it == g_entryNumToOverlay().end()) return; packPath = it->second->packPath; label = it->second->arcPath.filename().string(); } auto arcSpan = std::span{static_cast(arcBytes), arcSize}; { std::lock_guard lk{g_cacheMutex}; register_hd_arc_range_locked(arcSpan.data(), arcSpan.size(), label); } auto hdPack = load_pack_cached(packPath); if (hdPack == nullptr) { return; } auto hdFiles = parseRarcFiles(std::span(arcSpan.data(), arcSpan.size())); register_hd_textures_for_arc(arcSpan, hdFiles, *hdPack, label); } void register_copied_hd_resource(s32 entryNum, std::string_view resourceName, void* buffer, size_t resourceSize) { if (entryNum < 0 || buffer == nullptr || resourceSize < 0x20) return; const bool isBti = endsWithSuffixCI(resourceName, ".bti"); const bool isBmd = endsWithSuffixCI(resourceName, ".bmd") || endsWithSuffixCI(resourceName, ".bdl"); if (!isBti && !isBmd) 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; } if (isBti) { register_hd_bti_replacement_for_buffer(*hdPack, resourceName, buffer, resourceSize, true); } else { register_hd_bmd_textures_for_buffer(*hdPack, resourceName, buffer, resourceSize, true); } } std::optional find_registered_hd_archive_remaining(const void* ptr) { if (ptr == nullptr) return std::nullopt; const auto p = reinterpret_cast(ptr); std::lock_guard lk{g_cacheMutex}; for (const auto& range : g_arcRanges()) { const auto begin = reinterpret_cast(range.begin); const auto end = begin + range.size; if (p >= begin && p < end) { return end - p; } } return std::nullopt; } }