Replace textures from mounted JKRArchives

This commit is contained in:
Luke Street
2026-05-30 16:13:14 -06:00
parent 458ffb17c2
commit b6b71a32c7
3 changed files with 220 additions and 36 deletions
@@ -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;
}
+185 -36
View File
@@ -52,6 +52,11 @@ std::list<std::vector<u8>>& g_textureBuffers() {
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;
@@ -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<TphdPack> load_pack_from_file(const std::filesystem::path& path) {
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) {
@@ -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<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;
}
// 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<HdOverlayEntry*>(userData);
if (entry == nullptr) return nullptr;
@@ -741,6 +843,7 @@ void rebuild_hd_overlay_locked() {
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);
@@ -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<std::vector<u8>*> 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<std::vector<u8>*> 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<TphdPack> 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<u8>* mountBytes;
@@ -861,7 +985,7 @@ std::optional<std::vector<u8>*> try_load_hd_archive(std::string_view gcPath) {
filename, static_cast<const void*>(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<size_t> find_registered_hd_archive_remaining(const void* ptr) {
if (ptr == nullptr) return std::nullopt;
+5
View File
@@ -25,6 +25,11 @@ std::optional<std::vector<u8>*> 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<size_t> find_registered_hd_archive_remaining(const void* ptr);