Files
dusklight/src/dusk/tphd/HdAssetLayer.cpp
T

1043 lines
35 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "HdAssetLayer.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <list>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <string>
#include <system_error>
#include <unordered_map>
#include <vector>
#include <aurora/dvd.h>
#include <aurora/texture.hpp>
#include <dolphin/dvd.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_iostream.h>
#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<std::vector<u8>>& g_mountBuffers() {
static auto* p = new std::list<std::vector<u8>>{};
return *p;
}
std::list<std::vector<u8>>& g_textureBuffers() {
static auto* p = new std::list<std::vector<u8>>{};
return *p;
}
std::unordered_map<std::string, std::shared_ptr<const TphdPack>>& g_packCache() {
static auto* p = new std::unordered_map<std::string, std::shared_ptr<const TphdPack>>{};
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<HdArcRange>& g_arcRanges() {
static auto* p = new std::vector<HdArcRange>{};
return *p;
}
struct HdOverlayEntry {
std::string dvdPath;
std::filesystem::path arcPath;
std::filesystem::path packPath;
size_t size = 0;
};
std::list<HdOverlayEntry>& g_overlayEntries() {
static auto* p = new std::list<HdOverlayEntry>{};
return *p;
}
std::unordered_map<s32, HdOverlayEntry*>& g_entryNumToOverlay() {
static auto* p = new std::unordered_map<s32, HdOverlayEntry*>{};
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<SDL_IOStream, SDL_IODeleter>;
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<size_t> 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<std::vector<u8>> tryDecodeYaz0(std::span<const u8> bytes) {
if (bytes.size() < sizeof(Yaz0Header) ||
std::memcmp(bytes.data(), "Yaz0", 4) != 0) {
return std::nullopt;
}
ZoneScoped;
const auto* hdr = reinterpret_cast<const Yaz0Header*>(bytes.data());
const u32 expandedSize = hdr->decompressedSize;
std::vector<u8> decoded(expandedSize);
JKRDecomp::decodeSZS(const_cast<u8*>(bytes.data()), decoded.data(),
expandedSize, 0);
return decoded;
}
std::optional<std::vector<u8>> 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<u8> 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<TphdPack> 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<const TphdPack> 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<TphdPack>(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/.../<arc-rel>.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<u8> expandR5G6B5toRgba8(std::span<const u8> in, u32 width, u32 height) {
std::vector<u8> out(static_cast<size_t>(width) * height * 4);
const size_t pixelCount = static_cast<size_t>(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<u8>((px >> 11) & 0x1F);
u8 g6 = static_cast<u8>((px >> 5) & 0x3F);
u8 r5 = static_cast<u8>(px & 0x1F);
out[i * 4 + 0] = static_cast<u8>((r5 << 3) | (r5 >> 2));
out[i * 4 + 1] = static_cast<u8>((g6 << 2) | (g6 >> 4));
out[i * 4 + 2] = static_cast<u8>((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<u8> expandIA4toRgba8(std::span<const u8> in, u32 width, u32 height) {
std::vector<u8> out(static_cast<size_t>(width) * height * 4);
const size_t pixelCount = static_cast<size_t>(width) * height;
for (size_t i = 0; i < pixelCount && i < in.size(); ++i) {
u8 b = in[i];
u8 A = static_cast<u8>((b & 0xF0) | (b >> 4));
u8 I = static_cast<u8>(((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<u8> applyExpansion(Expansion exp, std::vector<u8> 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<addrlib::TileMode>(s.tileMode);
return d;
}
const addrlib::SurfaceInfoIn si{
.width = s.width,
.height = s.height,
.bpp = bpp,
.mipLevel = level,
.tileMode = static_cast<addrlib::TileMode>(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<const u8> 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<u32>(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<u8> 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<const u8> 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<ResTIMG*>(buffer);
timg->imageOffset = 0x20;
const u8 hdMips = static_cast<u8>(std::clamp<u32>(s.mipCount, 1u, 11u));
timg->mipmapCount = hdMips;
timg->maxLOD = static_cast<s8>((hdMips - 1) * 8);
registerHdSurface(*m, s, static_cast<u8*>(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<const u8> 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<const J3DModelFileData*>(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<const J3DModelBlock*>(bmd.data() + pos);
const u32 blockSize = blk->mBlockSize;
if (blk->mBlockType == 'TEX1') {
const auto* tex1 = reinterpret_cast<const J3DTextureBlock*>(bmd.data() + pos);
const u16 numTex = tex1->mTextureNum;
if (slotIdx >= numTex) return 0;
const size_t btiAbs = pos + static_cast<u32>(tex1->mpTextureRes) + slotIdx * 0x20;
if (btiAbs + 0x20 > bmd.size()) return 0;
return static_cast<u32>(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<u8> bmdBytes(static_cast<u8*>(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<ResTIMG*>(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<s32>(newImgOff);
const u8 hdMips = static_cast<u8>(std::clamp<u32>(s.mipCount, 1u, 11u));
timg->mipmapCount = hdMips;
timg->maxLOD = static_cast<s8>((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<ArcFileInfo> parseRarcFiles(std::span<const u8> arc) {
std::vector<ArcFileInfo> 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<const SArcHeader*>(arc.data());
const auto* dataInfo = reinterpret_cast<const SArcDataInfo*>(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<size_t>(bufferEnd - start));
const u8* terminator = nul ? static_cast<const u8*>(nul) : bufferEnd;
return std::string(reinterpret_cast<const char*>(start),
static_cast<size_t>(terminator - start));
};
const auto* nodes = reinterpret_cast<const JKRArchive::SDIDirEntry*>(
arc.data() + nodeTbl);
const auto* files = reinterpret_cast<const JKRArchive::SDIFileEntry*>(
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<u8>(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<u32>(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<u8> arcBytes, const std::vector<ArcFileInfo>& 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<HdOverlayEntry*>(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<SDL_IOStream*>(handle);
if (stream == nullptr) return;
SDL_CloseIO(stream);
}
int64_t overlay_read(void* handle, uint8_t* buf, size_t len) {
auto* stream = static_cast<SDL_IOStream*>(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<int64_t>(got);
}
int64_t overlay_seek(void* handle, int64_t offset, int32_t whence) {
auto* stream = static_cast<SDL_IOStream*>(handle);
if (stream == nullptr) {
return -1;
}
const Sint64 pos = SDL_SeekIO(stream, offset, static_cast<SDL_IOWhence>(whence));
return pos < 0 ? -1 : static_cast<int64_t>(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<AuroraOverlayFile> overlayFiles;
std::vector<HdOverlayEntry*> 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<std::vector<u8>*> 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<const u8>(
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<u8>* 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<const void*>(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<uint8_t*>(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<const u8>(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<size_t> find_registered_hd_archive_remaining(const void* ptr) {
if (ptr == nullptr) return std::nullopt;
const auto p = reinterpret_cast<uintptr_t>(ptr);
std::lock_guard lk{g_cacheMutex};
for (const auto& range : g_arcRanges()) {
const auto begin = reinterpret_cast<uintptr_t>(range.begin);
const auto end = begin + range.size;
if (p >= begin && p < end) {
return end - p;
}
}
return std::nullopt;
}
}