texture replacement 2

texture replacement 2
This commit is contained in:
Jessica_Natalia
2026-08-15 22:30:36 -03:00
parent 6cca9622e7
commit 179f059474
3 changed files with 26 additions and 94 deletions
-19
View File
@@ -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<std::size_t>::max()) {
const auto span = static_cast<std::size_t>(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;
}
+20 -58
View File
@@ -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<std::uint64_t, TextureIndexEntry> by_content;
// whole-raster content key -> index entry
std::unordered_map<std::uint64_t, TextureIndexEntry> by_full_content;
std::uint64_t matched_full{};
// upper-cased internal name -> .dds path supplied by the user
std::unordered_map<std::string, std::filesystem::path> overrides;
// Decoded .dds keyed by upper-cased name. unordered_map keeps element
@@ -50,11 +44,8 @@ struct State {
std::unordered_map<std::string, DecodedImage> decoded;
std::unordered_set<std::string> failed_decodes;
std::unordered_set<std::string> indexed_archives;
std::unordered_set<std::uint64_t> 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<std::uint64_t>(length - index) << 56u));
}
mix(width);
mix(height);
mix(depth);
@@ -440,11 +445,9 @@ std::vector<TextureIndexEntry> texture_replacement_parse_tex_chunk(
entry.archive_offset = record.data;
entry.raster_size = base_bytes;
const auto raster = static_cast<std::size_t>(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<std::size_t>(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<std::mutex> 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()
+6 -17
View File
@@ -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{};