From 179f059474c194e5da4524b89a0bb4b1bcee2954 Mon Sep 17 00:00:00 2001 From: Jessica_Natalia Date: Sat, 15 Aug 2026 22:30:36 -0300 Subject: [PATCH] texture replacement 2 texture replacement 2 --- profiles/vcs/host/ge_renderer.cpp | 19 ----- profiles/vcs/host/vcs_texture_replacement.cpp | 78 +++++-------------- profiles/vcs/host/vcs_texture_replacement.hpp | 23 ++---- 3 files changed, 26 insertions(+), 94 deletions(-) diff --git a/profiles/vcs/host/ge_renderer.cpp b/profiles/vcs/host/ge_renderer.cpp index 19b8bd5..8f41647 100644 --- a/profiles/vcs/host/ge_renderer.cpp +++ b/profiles/vcs/host/ge_renderer.cpp @@ -4195,25 +4195,6 @@ bool render_ge_primitive(psprecomp::GuestMemory &memory, if (part == 0u) continue; any_signature = true; signature ^= part + 0x9E3779B97F4A7C15ull + (signature << 6u) + (signature >> 2u); - // Offer level 0 to the DDS replacement index. This site fires - // once per texture that is not already resident for the frame, - // so it identifies rather than running per draw. - if (level == 0u) { - const std::uint64_t source_bytes = texture_source_byte_size(source); - if (source_bytes != 0u && - source_bytes <= std::numeric_limits::max()) { - const auto span = static_cast(source_bytes); - // The archives store palettized art only, so formats 4 - // and 5 are the only ones that can ever match an index - // entry. Anything else reports depth 0 and simply misses. - const std::uint32_t depth = source.format == 4u ? 4u - : source.format == 5u ? 8u - : 0u; - texture_replacement_observe_texture( - memory.raw_pointer(source.base, span), span, - source.width, source.height, depth); - } - } } gpu_draw.texture_content_signature = any_signature ? signature : 0u; } diff --git a/profiles/vcs/host/vcs_texture_replacement.cpp b/profiles/vcs/host/vcs_texture_replacement.cpp index d18579a..71ff5ac 100644 --- a/profiles/vcs/host/vcs_texture_replacement.cpp +++ b/profiles/vcs/host/vcs_texture_replacement.cpp @@ -22,10 +22,6 @@ constexpr std::uint32_t kTexIdent = 0x00746578u; // only ever begin at one. Scanning by sector rather than byte keeps a false // positive from a random run of bytes inside model or collision data. constexpr std::uint64_t kSectorSize = 0x800u; -// Enough leading raster to identify a texture without hashing megabytes. The -// data is swizzled 4/8bpp indices, so the first block rows already differ -// between any two distinct textures in practice. -constexpr std::size_t kKeyBytes = 1024u; struct DecodedImage { std::uint32_t width{}; @@ -38,10 +34,8 @@ struct State { bool initialized{}; bool enabled{}; std::filesystem::path directory; - // content key -> index entry, one map per key strength - std::unordered_map by_content; + // whole-raster content key -> index entry std::unordered_map by_full_content; - std::uint64_t matched_full{}; // upper-cased internal name -> .dds path supplied by the user std::unordered_map overrides; // Decoded .dds keyed by upper-cased name. unordered_map keeps element @@ -50,11 +44,8 @@ struct State { std::unordered_map decoded; std::unordered_set failed_decodes; std::unordered_set indexed_archives; - std::unordered_set reported_hits; std::uint64_t textures_indexed{}; std::uint64_t containers_indexed{}; - std::uint64_t observed_textures{}; - std::uint64_t matched_textures{}; std::uint64_t substituted_textures{}; }; @@ -361,7 +352,21 @@ std::uint64_t texture_replacement_content_key(const std::uint8_t *bytes, hash ^= value; hash *= 0x100000001B3ull; }; - for (std::size_t index = 0; index < length; ++index) mix(bytes[index]); + // Eight bytes per iteration. A byte at a time makes the loop a chain of + // dependent multiplies -- the same trap ge_renderer's own signature hash + // documents avoiding, and hashing a whole raster that way is thousands of + // them per texture. + std::size_t index = 0u; + for (; index + 8u <= length; index += 8u) { + std::uint64_t word{}; + std::memcpy(&word, bytes + index, sizeof(word)); + mix(word); + } + if (index < length) { + std::uint64_t tail = 0u; + std::memcpy(&tail, bytes + index, length - index); + mix(tail ^ (static_cast(length - index) << 56u)); + } mix(width); mix(height); mix(depth); @@ -440,11 +445,9 @@ std::vector texture_replacement_parse_tex_chunk( entry.archive_offset = record.data; entry.raster_size = base_bytes; - const auto raster = static_cast(base_bytes); - entry.content_key = texture_replacement_content_key( - chunk + record.data, raster, entry.width, entry.height, entry.depth, kKeyBytes); entry.full_key = texture_replacement_content_key( - chunk + record.data, raster, entry.width, entry.height, entry.depth, 0u); + chunk + record.data, static_cast(base_bytes), + entry.width, entry.height, entry.depth, 0u); out.push_back(std::move(entry)); } return out; @@ -483,8 +486,7 @@ void texture_replacement_index_archive(const std::filesystem::path &path) noexce // A duplicate key usually means two textures are byte-identical // -- the game ships the same art under several names. Keep the // first; they cannot be told apart by content anyway. - s.by_full_content.emplace(entry.full_key, entry); - s.by_content.emplace(entry.content_key, std::move(entry)); + s.by_full_content.emplace(entry.full_key, std::move(entry)); } } s.containers_indexed += containers; @@ -500,42 +502,6 @@ void texture_replacement_index_archive(const std::filesystem::path &path) noexce } } -void texture_replacement_observe_texture(const std::uint8_t *pixels, - std::size_t size, - std::uint32_t width, - std::uint32_t height, - std::uint32_t depth) noexcept { - State &s = state(); - try { - std::lock_guard guard(s.mutex); - if (!s.initialized || !s.enabled || pixels == nullptr || size == 0u) return; - ++s.observed_textures; - const std::uint64_t key = texture_replacement_content_key( - pixels, size, width, height, depth, kKeyBytes); - const std::uint64_t full = texture_replacement_content_key( - pixels, size, width, height, depth, 0u); - const bool full_matched = s.by_full_content.count(full) != 0u; - if (full_matched) ++s.matched_full; - const auto found = s.by_content.find(key); - if (found == s.by_content.end()) return; - ++s.matched_textures; - // One line per distinct texture, not per draw. - if (!s.reported_hits.insert(key).second) return; - std::ostringstream message; - message << "texture match name=\"" << found->second.name << "\" " - << found->second.width << 'x' << found->second.height - << " depth=" << found->second.depth - << " archive_bytes=" << found->second.raster_size - << " vram_bytes=" << size - << (found->second.raster_size == size ? " size=exact" : " size=differs") - << (full_matched ? " key=full" : " key=leading_only") - << (s.overrides.count(found->second.name) != 0u ? " override=yes" - : " override=no"); - runtime_log_line(message.str()); - } catch (...) { - } -} - bool texture_replacement_lookup(const std::uint8_t *pixels, std::size_t size, std::uint32_t width, std::uint32_t height, std::uint32_t depth, TextureReplacement &out) noexcept { @@ -600,7 +566,7 @@ void texture_replacement_log_summary() noexcept { std::size_t matched_overrides = 0u; for (const auto &[name, path] : s.overrides) { (void)path; - for (const auto &[key, entry] : s.by_content) { + for (const auto &[key, entry] : s.by_full_content) { (void)key; if (entry.name == name) { ++matched_overrides; @@ -611,10 +577,6 @@ void texture_replacement_log_summary() noexcept { std::ostringstream message; message << "texture replacement summary containers=" << s.containers_indexed << " indexed=" << s.textures_indexed - << " distinct_seen=" << s.reported_hits.size() - << " observed=" << s.observed_textures - << " matched=" << s.matched_textures - << " matched_full_key=" << s.matched_full << " substituted=" << s.substituted_textures << " decoded_dds=" << s.decoded.size() << " failed_dds=" << s.failed_decodes.size() diff --git a/profiles/vcs/host/vcs_texture_replacement.hpp b/profiles/vcs/host/vcs_texture_replacement.hpp index 42f75eb..96c57ea 100644 --- a/profiles/vcs/host/vcs_texture_replacement.hpp +++ b/profiles/vcs/host/vcs_texture_replacement.hpp @@ -29,14 +29,12 @@ struct TextureIndexEntry { std::uint32_t mipmaps{}; std::uint64_t archive_offset{}; std::uint64_t raster_size{}; - // Two keys on purpose. The leading-bytes key tolerates the guest padding a - // texture's pitch on upload; the whole-raster key cannot, but discriminates - // far better. Measured on this game's main archive, the leading-bytes key - // puts three genuinely different textures into shared buckets -- all of them - // radar tiles, which share a uniform first kilobyte. Stage 1 carries both and - // reports which one the GE actually matches, so stage 2 can commit to the - // strongest key the guest's upload behaviour allows. - std::uint64_t content_key{}; + // Hash of the whole raster. A leading-bytes variant was carried during + // bring-up to find out whether the guest pads a texture's pitch on upload; + // it does not -- every texture in this game reaches VRAM byte-identical to + // its archive copy -- so the strong key is the one that survived. It has to + // be: hashing only the first kilobyte puts several radar tiles in shared + // buckets and would swap the wrong one. std::uint64_t full_key{}; }; @@ -45,15 +43,6 @@ struct TextureIndexEntry { // backup copies sitting next to the real files out of the index. void texture_replacement_index_archive(const std::filesystem::path &path) noexcept; -// Offers the raster bytes the GE is about to sample. Stage 1 only identifies and -// records; no substitution happens yet. Dimensions and bit depth participate in -// the key, so two textures now have to agree on shape *and* content to collide. -void texture_replacement_observe_texture(const std::uint8_t *pixels, - std::size_t size, - std::uint32_t width, - std::uint32_t height, - std::uint32_t depth) noexcept; - // A decoded replacement, owned by the module and stable for the process's life. struct TextureReplacement { const std::byte *rgba{};