mirror of
https://github.com/jessicanataliagta/PSPRecomp
synced 2026-09-26 16:49:34 -04:00
texture replacement 2
texture replacement 2
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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{};
|
||||
|
||||
Reference in New Issue
Block a user