#include "hd_texture_pack.h" #ifndef HD_TEXTURE_PACK_DISABLE_PNG_DECODE #include "../third_party/stb_image.h" /* declarations only; implementation is shared */ #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #endif namespace fs = std::filesystem; namespace { constexpr unsigned kVramWidth = 1024; constexpr unsigned kVramHeight = 512; /* GPU primitives invalidate VRAM constantly. Keep a coarse spatial index so * the common case does not walk every historical upload. The index is only * a broad phase; fragment rectangles remain authoritative below. */ constexpr unsigned kUploadIndexTileSize = 32; constexpr unsigned kUploadIndexCols = (kVramWidth + kUploadIndexTileSize - 1) / kUploadIndexTileSize; constexpr unsigned kUploadIndexRows = (kVramHeight + kUploadIndexTileSize - 1) / kUploadIndexTileSize; constexpr size_t kUploadIndexCells = size_t{kUploadIndexCols} * kUploadIndexRows; constexpr size_t kVramWords = size_t{kVramWidth} * kVramHeight; constexpr size_t kDefaultDecodeBudget = size_t{64} * 1024 * 1024; constexpr uint32_t kTrackingStateMagic = 0x31544448u; /* "HDT1" LE */ constexpr uint32_t kTrackingStateVersion = 1; constexpr size_t kTrackingStateHeaderBytes = 24; constexpr size_t kTrackingStateUploadBytes = 20; constexpr size_t kTrackingStateFragmentBytes = 12; constexpr size_t kMaxTrackingStateBytes = size_t{8} * 1024 * 1024; constexpr uint32_t kMaxTrackingStateUploads = 8192; constexpr uint32_t kMaxTrackingStateFragments = 262144; #ifndef HD_TEXTURE_PACK_DISABLE_PNG_DECODE constexpr size_t kMaxDecodeQueue = 32; constexpr size_t kMaxPngFileBytes = size_t{64} * 1024 * 1024; #endif uint64_t make_key(uint32_t texture_hash, uint32_t palette_hash) { return (uint64_t{texture_hash} << 32) | palette_hash; } void write_error(char* error, size_t capacity, const std::string& message) { if (!error || capacity == 0) return; std::snprintf(error, capacity, "%s", message.c_str()); } std::string trim(std::string value) { const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char c) { return std::isspace(c) != 0; }); const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char c) { return std::isspace(c) != 0; }).base(); if (first >= last) return {}; return std::string(first, last); } std::string ascii_lower(std::string value) { for (char& c : value) c = static_cast(std::tolower(static_cast(c))); return value; } bool ends_with_ci(const std::string& value, const char* suffix) { const std::string lower = ascii_lower(value); const std::string wanted = ascii_lower(suffix); return lower.size() >= wanted.size() && lower.compare(lower.size() - wanted.size(), wanted.size(), wanted) == 0; } bool parse_hex_component(const std::string& text, uint32_t* out) { if (!out || text.empty() || text.size() > 8) return false; uint32_t value = 0; for (const unsigned char c : text) { unsigned digit; if (c >= '0' && c <= '9') digit = c - '0'; else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10; else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10; else return false; value = (value << 4) | digit; } *out = value; return true; } bool parse_key_text(const std::string& text, uint32_t* texture_hash, uint32_t* palette_hash) { const size_t dash = text.find('-'); if (dash == std::string::npos || text.find('-', dash + 1) != std::string::npos) return false; return parse_hex_component(text.substr(0, dash), texture_hash) && parse_hex_component(text.substr(dash + 1), palette_hash); } bool parse_png_filename(const fs::path& path, uint32_t* texture_hash, uint32_t* palette_hash) { if (ascii_lower(path.extension().string()) != ".png") return false; return parse_key_text(path.stem().string(), texture_hash, palette_hash); } const std::array& crc_table() { static const std::array table = [] { std::array result{}; for (uint32_t i = 0; i < result.size(); ++i) { uint32_t value = i; for (unsigned bit = 0; bit < 8; ++bit) value = (value >> 1) ^ ((value & 1) ? 0xEDB88320u : 0u); result[i] = value; } return result; }(); return table; } uint32_t crc_byte(uint32_t crc, uint8_t value) { return crc_table()[(crc ^ value) & 0xFFu] ^ (crc >> 8); } struct Rect { unsigned x = 0; unsigned y = 0; unsigned width = 0; unsigned height = 0; }; struct Fragment { Rect rect; unsigned source_x = 0; unsigned source_y = 0; }; bool intersects(const Rect& a, const Rect& b) { return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height; } bool contains_point(const Rect& rect, unsigned x, unsigned y) { return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; } std::vector physical_fragments(unsigned x, unsigned y, unsigned width, unsigned height) { std::vector result; if (width == 0 || height == 0 || width > kVramWidth || height > kVramHeight) return result; const unsigned px = x & (kVramWidth - 1); const unsigned py = y & (kVramHeight - 1); const unsigned first_w = std::min(width, kVramWidth - px); const unsigned first_h = std::min(height, kVramHeight - py); const std::array, 2> xs{{ {px, first_w}, {0, width - first_w} }}; const std::array, 2> ys{{ {py, first_h}, {0, height - first_h} }}; unsigned source_y = 0; for (const auto& yspan : ys) { if (yspan.second == 0) continue; unsigned source_x = 0; for (const auto& xspan : xs) { if (xspan.second != 0) { result.push_back({ {xspan.first, yspan.first, xspan.second, yspan.second}, source_x, source_y }); } source_x += xspan.second; } source_y += yspan.second; } return result; } std::vector physical_rects(unsigned x, unsigned y, unsigned width, unsigned height) { std::vector result; for (const Fragment& fragment : physical_fragments(x, y, width, height)) result.push_back(fragment.rect); return result; } std::vector subtract_fragment(const Fragment& original, const Rect& cut) { if (!intersects(original.rect, cut)) return {original}; const unsigned left = std::max(original.rect.x, cut.x); const unsigned right = std::min(original.rect.x + original.rect.width, cut.x + cut.width); const unsigned top = std::max(original.rect.y, cut.y); const unsigned bottom = std::min(original.rect.y + original.rect.height, cut.y + cut.height); std::vector result; const auto add = [&](unsigned x, unsigned y, unsigned width, unsigned height) { if (width == 0 || height == 0) return; result.push_back({ {x, y, width, height}, original.source_x + x - original.rect.x, original.source_y + y - original.rect.y }); }; add(original.rect.x, original.rect.y, original.rect.width, top - original.rect.y); add(original.rect.x, bottom, original.rect.width, original.rect.y + original.rect.height - bottom); add(original.rect.x, top, left - original.rect.x, bottom - top); add(right, top, original.rect.x + original.rect.width - right, bottom - top); return result; } std::vector subtract_rect(const Rect& original, const Rect& cut) { const Fragment source{original, 0, 0}; std::vector result; for (const Fragment& piece : subtract_fragment(source, cut)) result.push_back(piece.rect); return result; } struct Upload { uint64_t serial = 0; uint32_t hash = 0; uint16_t width = 0; uint16_t height = 0; /* Conservative broad phase for GPU-draw invalidation. Fragment coverage * remains authoritative; this union only avoids walking unrelated uploads. */ Rect bounds{}; std::vector fragments; /* Runtime-only broad-phase membership; never serialized. */ std::vector index_tiles; }; Rect fragment_bounds(const std::vector& fragments) { Rect bounds{}; for (const Fragment& fragment : fragments) { if (bounds.width == 0 || bounds.height == 0) { bounds = fragment.rect; continue; } const unsigned right = std::max(bounds.x + bounds.width, fragment.rect.x + fragment.rect.width); const unsigned bottom = std::max(bounds.y + bounds.height, fragment.rect.y + fragment.rect.height); bounds.x = std::min(bounds.x, fragment.rect.x); bounds.y = std::min(bounds.y, fragment.rect.y); bounds.width = right - bounds.x; bounds.height = bottom - bounds.y; } return bounds; } void append_u16_le(std::vector& output, uint16_t value) { output.push_back(static_cast(value)); output.push_back(static_cast(value >> 8)); } void append_u32_le(std::vector& output, uint32_t value) { append_u16_le(output, static_cast(value)); append_u16_le(output, static_cast(value >> 16)); } void append_u64_le(std::vector& output, uint64_t value) { append_u32_le(output, static_cast(value)); append_u32_le(output, static_cast(value >> 32)); } bool take_u16_le(const uint8_t** cursor, const uint8_t* end, uint16_t* out) { if (!cursor || !*cursor || !out || end - *cursor < 2) return false; const uint8_t* p = *cursor; *out = static_cast(p[0]) | static_cast(static_cast(p[1]) << 8); *cursor += 2; return true; } bool take_u32_le(const uint8_t** cursor, const uint8_t* end, uint32_t* out) { uint16_t low = 0, high = 0; if (!take_u16_le(cursor, end, &low) || !take_u16_le(cursor, end, &high)) return false; *out = static_cast(low) | (static_cast(high) << 16); return true; } bool take_u64_le(const uint8_t** cursor, const uint8_t* end, uint64_t* out) { uint32_t low = 0, high = 0; if (!take_u32_le(cursor, end, &low) || !take_u32_le(cursor, end, &high)) return false; *out = static_cast(low) | (static_cast(high) << 32); return true; } bool valid_fragment(const Upload& upload, const Fragment& fragment) { const Rect& rect = fragment.rect; return rect.width != 0 && rect.height != 0 && rect.x < kVramWidth && rect.y < kVramHeight && rect.width <= kVramWidth - rect.x && rect.height <= kVramHeight - rect.y && fragment.source_x < upload.width && fragment.source_y < upload.height && rect.width <= unsigned{upload.width} - fragment.source_x && rect.height <= unsigned{upload.height} - fragment.source_y; } bool parse_tracking_state(const uint8_t* data, size_t size, uint64_t* out_next_serial, std::vector* out_uploads) { if (!data || size < kTrackingStateHeaderBytes || size > kMaxTrackingStateBytes || !out_next_serial) return false; const uint8_t* cursor = data; const uint8_t* end = data + size; uint32_t magic = 0, version = 0, upload_count = 0, reserved = 0; uint64_t next_serial = 0, max_serial = 0; uint64_t total_fragments = 0; if (!take_u32_le(&cursor, end, &magic) || !take_u32_le(&cursor, end, &version) || !take_u64_le(&cursor, end, &next_serial) || !take_u32_le(&cursor, end, &upload_count) || !take_u32_le(&cursor, end, &reserved) || magic != kTrackingStateMagic || version != kTrackingStateVersion || next_serial == 0 || reserved != 0 || upload_count > kMaxTrackingStateUploads) return false; std::vector parsed; if (out_uploads) parsed.reserve(upload_count); std::vector serials; serials.reserve(upload_count); for (uint32_t i = 0; i < upload_count; ++i) { Upload upload; uint32_t fragment_count = 0; if (!take_u64_le(&cursor, end, &upload.serial) || !take_u32_le(&cursor, end, &upload.hash) || !take_u16_le(&cursor, end, &upload.width) || !take_u16_le(&cursor, end, &upload.height) || !take_u32_le(&cursor, end, &fragment_count) || upload.serial == 0 || upload.width == 0 || upload.height == 0 || upload.width > kVramWidth || upload.height > kVramHeight || fragment_count == 0 || fragment_count > kMaxTrackingStateFragments - total_fragments) return false; total_fragments += fragment_count; upload.fragments.reserve(fragment_count); for (uint32_t j = 0; j < fragment_count; ++j) { Fragment fragment; uint16_t x = 0, y = 0, width = 0, height = 0; uint16_t source_x = 0, source_y = 0; if (!take_u16_le(&cursor, end, &x) || !take_u16_le(&cursor, end, &y) || !take_u16_le(&cursor, end, &width) || !take_u16_le(&cursor, end, &height) || !take_u16_le(&cursor, end, &source_x) || !take_u16_le(&cursor, end, &source_y)) return false; fragment.rect = Rect{x, y, width, height}; fragment.source_x = source_x; fragment.source_y = source_y; if (!valid_fragment(upload, fragment)) return false; for (const Fragment& prior : upload.fragments) if (intersects(prior.rect, fragment.rect)) return false; upload.fragments.push_back(fragment); } upload.bounds = fragment_bounds(upload.fragments); if (std::find(serials.begin(), serials.end(), upload.serial) != serials.end()) return false; serials.push_back(upload.serial); max_serial = std::max(max_serial, upload.serial); if (out_uploads) parsed.push_back(std::move(upload)); } if (cursor != end || next_serial <= max_serial) return false; *out_next_serial = next_serial; if (out_uploads) *out_uploads = std::move(parsed); return true; } bool covered_by_upload(const Upload& upload, const std::vector& query) { for (const Rect& wanted : query) { std::vector uncovered{wanted}; for (const Fragment& fragment : upload.fragments) { std::vector next; for (const Rect& piece : uncovered) { std::vector remainder = subtract_rect(piece, fragment.rect); next.insert(next.end(), remainder.begin(), remainder.end()); } uncovered.swap(next); if (uncovered.empty()) break; } if (!uncovered.empty()) return false; } return true; } struct EntryRecord { uint32_t texture_hash = 0; uint32_t palette_hash = 0; std::string replacement_path; std::string logical_path; bool ambiguous = false; }; struct DecodedImage { std::vector rgba; uint32_t width = 0; uint32_t height = 0; }; enum class DecodeState { Queued, Loading, Ready, Failed }; struct DecodeItem { DecodeState state = DecodeState::Queued; std::string path; std::shared_ptr image; size_t bytes = 0; uint64_t last_used = 0; }; struct DecodeCache { std::mutex mutex; std::condition_variable wake; std::unordered_map items; std::deque queue; std::thread worker; size_t budget = kDefaultDecodeBudget; size_t used = 0; uint64_t tick = 0; bool started = false; bool stop = false; ~DecodeCache() { { std::lock_guard lock(mutex); stop = true; } wake.notify_all(); if (worker.joinable()) worker.join(); } }; struct PixelLease { std::shared_ptr image; }; void evict_decode_cache(DecodeCache& cache, size_t incoming = 0) { while (cache.used + incoming > cache.budget) { auto victim = cache.items.end(); for (auto it = cache.items.begin(); it != cache.items.end(); ++it) { if (it->second.state != DecodeState::Ready) continue; if (victim == cache.items.end() || it->second.last_used < victim->second.last_used) victim = it; } if (victim == cache.items.end()) break; cache.used -= victim->second.bytes; cache.items.erase(victim); } } #ifndef HD_TEXTURE_PACK_DISABLE_PNG_DECODE void decode_worker(DecodeCache* cache) { #ifdef _WIN32 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); #endif for (;;) { uint64_t key = 0; std::string path; { std::unique_lock lock(cache->mutex); cache->wake.wait(lock, [&] { return cache->stop || !cache->queue.empty(); }); if (cache->stop) return; key = cache->queue.front(); cache->queue.pop_front(); auto it = cache->items.find(key); if (it == cache->items.end()) continue; it->second.state = DecodeState::Loading; path = it->second.path; } std::vector encoded; { std::ifstream input(path, std::ios::binary | std::ios::ate); std::streamoff length = -1; if (input) length = static_cast(input.tellg()); if (length > 0 && static_cast(length) <= kMaxPngFileBytes && static_cast(length) <= static_cast(std::numeric_limits::max())) { encoded.resize(static_cast(length)); input.seekg(0); if (!input.read(reinterpret_cast(encoded.data()), length)) encoded.clear(); } } int width = 0; int height = 0; int components = 0; stbi_uc* loaded = encoded.empty() ? nullptr : stbi_load_from_memory( encoded.data(), static_cast(encoded.size()), &width, &height, &components, 4); std::shared_ptr decoded; size_t bytes = 0; if (loaded && width > 0 && height > 0 && static_cast(width) * static_cast(height) <= std::numeric_limits::max() / 4) { bytes = static_cast(width) * static_cast(height) * 4; decoded = std::make_shared(); decoded->width = static_cast(width); decoded->height = static_cast(height); decoded->rgba.assign(loaded, loaded + bytes); } if (loaded) stbi_image_free(loaded); std::lock_guard lock(cache->mutex); auto it = cache->items.find(key); if (it == cache->items.end()) continue; if (!decoded || bytes > cache->budget) { it->second.state = DecodeState::Failed; continue; } evict_decode_cache(*cache, bytes); if (cache->used + bytes > cache->budget) { it->second.state = DecodeState::Failed; continue; } it->second.image = std::move(decoded); it->second.bytes = bytes; it->second.last_used = ++cache->tick; it->second.state = DecodeState::Ready; cache->used += bytes; } } #endif bool directory_has_pack_png(const fs::path& directory) { std::error_code ec; for (fs::directory_iterator it(directory, ec), end; !ec && it != end; it.increment(ec)) { if (!it->is_regular_file(ec)) continue; uint32_t texture_hash = 0; uint32_t palette_hash = 0; if (parse_png_filename(it->path(), &texture_hash, &palette_hash)) return true; } return false; } void parse_hashes_ini(const fs::path& path, std::unordered_map* mappings) { if (!mappings) return; std::ifstream input(path); if (!input) return; std::string line; while (std::getline(input, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); const std::string stripped = trim(line); if (stripped.empty() || stripped[0] == '#' || stripped[0] == '[') continue; const size_t equal = stripped.find('='); if (equal == std::string::npos) continue; uint32_t texture_hash = 0; uint32_t palette_hash = 0; if (!parse_key_text(trim(stripped.substr(0, equal)), &texture_hash, &palette_hash)) continue; const std::string logical = trim(stripped.substr(equal + 1)); if (!logical.empty()) (*mappings)[make_key(texture_hash, palette_hash)] = logical; } } std::vector query_rectangles(const HdTextureDrawQuery& query) { const unsigned pixels_per_word = query.depth == HD_TEXTURE_DEPTH_4BPP ? 4u : query.depth == HD_TEXTURE_DEPTH_8BPP ? 2u : 1u; std::vector> u_ranges; std::vector> v_ranges; if (query.u_first <= query.u_last) u_ranges.push_back({query.u_first, query.u_last}); else { u_ranges.push_back({query.u_first, 255}); u_ranges.push_back({0, query.u_last}); } if (query.v_first <= query.v_last) v_ranges.push_back({query.v_first, query.v_last}); else { v_ranges.push_back({query.v_first, 255}); v_ranges.push_back({0, query.v_last}); } std::vector result; for (const auto& vrange : v_ranges) { for (const auto& urange : u_ranges) { const unsigned first_word = urange.first / pixels_per_word; const unsigned last_word = urange.second / pixels_per_word; std::vector pieces = physical_rects( unsigned{query.page_x} + first_word, unsigned{query.page_y} + vrange.first, last_word - first_word + 1, vrange.second - vrange.first + 1); result.insert(result.end(), pieces.begin(), pieces.end()); } } return result; } void fill_entry(const EntryRecord& record, HdTexturePackEntry* entry) { if (!entry) return; entry->texture_hash = record.texture_hash; entry->palette_hash = record.palette_hash; entry->replacement_path = record.replacement_path.c_str(); entry->logical_path = record.logical_path.c_str(); } } // namespace struct HdTexturePack { std::string asset_root; std::string replacement_root; std::unordered_map entries; std::unordered_set texture_hashes; /* any palette */ uint32_t identity = 0; /* hd_texture_pack_identity */ size_t replacement_file_count = 0; size_t ambiguous_key_count = 0; size_t logical_mapping_count = 0; uint64_t next_upload_serial = 1; std::vector uploads; std::array, kUploadIndexCells> upload_index; std::unordered_map upload_by_serial; /* Conservative broad phase for the GPU primitive hot path. Most scene * draws target frame-buffer VRAM and cannot affect texture uploads; avoid * allocating cut/candidate vectors for those draws. This union may stay * larger after invalidation, but is reset with tracking and therefore can * only cause extra work, never a missed invalidation. */ Rect upload_bounds{}; DecodeCache decode; }; unsigned upload_index_cell(unsigned tx, unsigned ty) { return ty * kUploadIndexCols + tx; } void upload_index_clear(HdTexturePack* pack) { if (!pack) return; for (auto& bucket : pack->upload_index) bucket.clear(); pack->upload_by_serial.clear(); for (Upload& upload : pack->uploads) upload.index_tiles.clear(); } void upload_index_remove(HdTexturePack* pack, Upload& upload) { if (!pack) return; for (const uint16_t tile : upload.index_tiles) { auto& bucket = pack->upload_index[tile]; bucket.erase(std::remove(bucket.begin(), bucket.end(), upload.serial), bucket.end()); } upload.index_tiles.clear(); pack->upload_by_serial.erase(upload.serial); } void upload_index_add(HdTexturePack* pack, Upload& upload) { if (!pack || upload.fragments.empty() || upload.bounds.width == 0 || upload.bounds.height == 0) return; const unsigned first_x = upload.bounds.x / kUploadIndexTileSize; const unsigned first_y = upload.bounds.y / kUploadIndexTileSize; const unsigned last_x = std::min( kUploadIndexCols - 1, (upload.bounds.x + upload.bounds.width - 1) / kUploadIndexTileSize); const unsigned last_y = std::min( kUploadIndexRows - 1, (upload.bounds.y + upload.bounds.height - 1) / kUploadIndexTileSize); pack->upload_by_serial[upload.serial] = &upload; upload.index_tiles.reserve((last_x - first_x + 1) * (last_y - first_y + 1)); for (unsigned ty = first_y; ty <= last_y; ++ty) { for (unsigned tx = first_x; tx <= last_x; ++tx) { const unsigned tile = upload_index_cell(tx, ty); pack->upload_index[tile].push_back(upload.serial); upload.index_tiles.push_back(static_cast(tile)); } } } void upload_index_refresh(HdTexturePack* pack, Upload& upload) { upload_index_remove(pack, upload); upload_index_add(pack, upload); } void upload_index_rebuild(HdTexturePack* pack) { if (!pack) return; for (auto& bucket : pack->upload_index) bucket.clear(); pack->upload_by_serial.clear(); pack->upload_bounds = Rect{}; for (Upload& upload : pack->uploads) { upload.index_tiles.clear(); upload_index_add(pack, upload); if (pack->upload_bounds.width == 0 || pack->upload_bounds.height == 0) { pack->upload_bounds = upload.bounds; } else { const unsigned right = std::max( pack->upload_bounds.x + pack->upload_bounds.width, upload.bounds.x + upload.bounds.width); const unsigned bottom = std::max( pack->upload_bounds.y + pack->upload_bounds.height, upload.bounds.y + upload.bounds.height); pack->upload_bounds.x = std::min(pack->upload_bounds.x, upload.bounds.x); pack->upload_bounds.y = std::min(pack->upload_bounds.y, upload.bounds.y); pack->upload_bounds.width = right - pack->upload_bounds.x; pack->upload_bounds.height = bottom - pack->upload_bounds.y; } } } void upload_index_collect(const HdTexturePack* pack, const std::vector& query, std::vector* out_serials) { if (!pack || !out_serials) return; out_serials->clear(); for (const Rect& rect : query) { if (rect.width == 0 || rect.height == 0) continue; const unsigned first_x = rect.x / kUploadIndexTileSize; const unsigned first_y = rect.y / kUploadIndexTileSize; const unsigned last_x = std::min( kUploadIndexCols - 1, (rect.x + rect.width - 1) / kUploadIndexTileSize); const unsigned last_y = std::min( kUploadIndexRows - 1, (rect.y + rect.height - 1) / kUploadIndexTileSize); for (unsigned ty = first_y; ty <= last_y; ++ty) { for (unsigned tx = first_x; tx <= last_x; ++tx) { const auto& bucket = pack->upload_index[ upload_index_cell(tx, ty)]; out_serials->insert(out_serials->end(), bucket.begin(), bucket.end()); } } } std::sort(out_serials->begin(), out_serials->end()); out_serials->erase(std::unique(out_serials->begin(), out_serials->end()), out_serials->end()); } extern "C" { int hd_texture_pack_create(const char* explicit_root, HdTexturePack** out_pack, char* error, size_t error_capacity) { if (out_pack) *out_pack = nullptr; if (error && error_capacity) error[0] = '\0'; if (!out_pack) { write_error(error, error_capacity, "out_pack is null"); return 0; } try { const char* selected = explicit_root; if (!selected || !selected[0]) selected = std::getenv("PSXRECOMP_HD_TEXTURE_ROOT"); if (!selected || !selected[0]) { write_error(error, error_capacity, "HD asset root is unset (PSXRECOMP_HD_TEXTURE_ROOT)"); return 0; } std::error_code ec; fs::path input = fs::absolute(fs::path(selected), ec).lexically_normal(); if (ec || !fs::is_directory(input, ec)) { write_error(error, error_capacity, "HD asset root is not a directory"); return 0; } fs::path replacement; fs::path hashes_ini; if (directory_has_pack_png(input)) { replacement = input; hashes_ini = fs::is_regular_file(input / "Hashes.ini", ec) ? input / "Hashes.ini" : input.parent_path() / "Hashes.ini"; } else if (fs::is_regular_file(input / "Hashes.ini", ec)) { std::vector children; for (fs::directory_iterator it(input, ec), end; !ec && it != end; it.increment(ec)) { if (it->is_directory(ec) && ends_with_ci(it->path().filename().string(), "-texture-replacements")) children.push_back(it->path()); } std::sort(children.begin(), children.end()); if (children.size() != 1) { write_error(error, error_capacity, "pack root must contain exactly one *-texture-replacements directory"); return 0; } replacement = children.front(); hashes_ini = input / "Hashes.ini"; } else { write_error(error, error_capacity, "no numeric PNGs or Hashes.ini + replacement directory found"); return 0; } auto pack = std::make_unique(); pack->asset_root = input.string(); pack->replacement_root = replacement.lexically_normal().string(); std::unordered_map mappings; parse_hashes_ini(hashes_ini, &mappings); pack->logical_mapping_count = mappings.size(); std::vector files; for (fs::directory_iterator it(replacement, ec), end; !ec && it != end; it.increment(ec)) { if (!it->is_regular_file(ec)) continue; uint32_t texture_hash = 0; uint32_t palette_hash = 0; if (parse_png_filename(it->path(), &texture_hash, &palette_hash)) files.push_back(it->path()); } if (ec) { write_error(error, error_capacity, "failed to scan replacement directory"); return 0; } std::sort(files.begin(), files.end()); if (files.empty()) { write_error(error, error_capacity, "replacement directory has no numeric PNGs"); return 0; } for (const fs::path& path : files) { uint32_t texture_hash = 0; uint32_t palette_hash = 0; if (!parse_png_filename(path, &texture_hash, &palette_hash)) continue; ++pack->replacement_file_count; const uint64_t key = make_key(texture_hash, palette_hash); auto found = pack->entries.find(key); if (found != pack->entries.end()) { if (!found->second.ambiguous) { found->second.ambiguous = true; ++pack->ambiguous_key_count; } continue; } EntryRecord record; record.texture_hash = texture_hash; record.palette_hash = palette_hash; record.replacement_path = path.lexically_normal().string(); const auto mapping = mappings.find(key); if (mapping != mappings.end()) record.logical_path = mapping->second; pack->entries.emplace(key, std::move(record)); pack->texture_hashes.insert(static_cast(key >> 32)); } { /* Identity: FNV-1a over the sorted replacement keys (texture and * palette hash), so two loads of the same pack agree wherever it * lives, and a pack with other keys differs. */ std::vector keys; keys.reserve(pack->entries.size()); for (const auto& entry : pack->entries) keys.push_back(entry.first); std::sort(keys.begin(), keys.end()); uint32_t h = 2166136261u; for (const uint64_t key : keys) for (int i = 0; i < 8; ++i) { h ^= uint8_t(key >> (8 * i)); h *= 16777619u; } pack->identity = h ? h : 1u; } *out_pack = pack.release(); return 1; } catch (const std::exception& exception) { write_error(error, error_capacity, exception.what()); return 0; } } void hd_texture_pack_destroy(HdTexturePack* pack) { delete pack; } void hd_texture_pack_get_info(const HdTexturePack* pack, HdTexturePackInfo* out_info) { if (!out_info) return; std::memset(out_info, 0, sizeof(*out_info)); if (!pack) return; out_info->asset_root = pack->asset_root.c_str(); out_info->replacement_root = pack->replacement_root.c_str(); out_info->replacement_file_count = pack->replacement_file_count; out_info->unique_key_count = pack->entries.size() - pack->ambiguous_key_count; out_info->ambiguous_key_count = pack->ambiguous_key_count; out_info->logical_mapping_count = pack->logical_mapping_count; } int hd_texture_pack_lookup(const HdTexturePack* pack, uint32_t texture_hash, uint32_t palette_hash, HdTexturePackEntry* out_entry) { if (out_entry) std::memset(out_entry, 0, sizeof(*out_entry)); if (!pack) return HD_TEXTURE_LOOKUP_ERROR; const auto found = pack->entries.find(make_key(texture_hash, palette_hash)); if (found == pack->entries.end()) return HD_TEXTURE_LOOKUP_NONE; if (found->second.ambiguous) return HD_TEXTURE_LOOKUP_AMBIGUOUS; fill_entry(found->second, out_entry); return HD_TEXTURE_LOOKUP_FOUND; } uint32_t hd_texture_pack_identity(const HdTexturePack* pack) { return pack ? pack->identity : 0u; } int hd_texture_pack_has_texture(const HdTexturePack* pack, uint32_t texture_hash) { return pack && pack->texture_hashes.count(texture_hash) ? 1 : 0; } uint32_t hd_texture_crc32_words_le(const uint16_t* words, size_t word_count) { if (!words && word_count != 0) return 0; uint32_t crc = 0xFFFFFFFFu; for (size_t i = 0; i < word_count; ++i) { crc = crc_byte(crc, static_cast(words[i] & 0xFFu)); crc = crc_byte(crc, static_cast(words[i] >> 8)); } return crc ^ 0xFFFFFFFFu; } uint32_t hd_texture_hash_clut(const uint16_t* vram, size_t vram_word_count, uint16_t clut_x, uint16_t clut_y, uint8_t depth) { if (depth == HD_TEXTURE_DEPTH_16BPP) return 0; if (!vram || vram_word_count < kVramWords) return 0; const unsigned count = depth == HD_TEXTURE_DEPTH_4BPP ? 16u : depth == HD_TEXTURE_DEPTH_8BPP ? 256u : 0u; if (count == 0) return 0; std::array palette{}; const unsigned y = clut_y & (kVramHeight - 1); for (unsigned i = 0; i < count; ++i) palette[i] = vram[y * kVramWidth + ((unsigned{clut_x} + i) & (kVramWidth - 1))]; return hd_texture_crc32_words_le(palette.data(), count); } void hd_texture_pack_invalidate(HdTexturePack* pack, uint16_t x, uint16_t y, uint16_t width_words, uint16_t height) { if (!pack || width_words == 0 || height == 0 || width_words > kVramWidth || height > kVramHeight) return; bool erased_upload = false; if (pack->upload_bounds.width == 0 || pack->upload_bounds.height == 0) return; if ((unsigned{x} + width_words <= kVramWidth) && (unsigned{y} + height <= kVramHeight)) { const Rect cut{unsigned{x}, unsigned{y}, width_words, height}; if (!intersects(pack->upload_bounds, cut)) return; } const std::vector cuts = physical_rects(x, y, width_words, height); std::vector candidate_serials; upload_index_collect(pack, cuts, &candidate_serials); for (const uint64_t serial : candidate_serials) { auto indexed = pack->upload_by_serial.find(serial); if (indexed == pack->upload_by_serial.end() || !indexed->second) continue; Upload& upload = *indexed->second; for (const Rect& cut : cuts) { /* Most primitives are outside every tracked upload. The union is * conservative, so this can only skip work that is provably * unrelated; individual fragments remain the source of truth. */ if (!intersects(upload.bounds, cut)) continue; std::vector next; next.reserve(upload.fragments.size() + 4); for (const Fragment& fragment : upload.fragments) { if (!intersects(fragment.rect, cut)) { next.push_back(fragment); continue; } std::vector pieces = subtract_fragment(fragment, cut); next.insert(next.end(), pieces.begin(), pieces.end()); } upload.fragments.swap(next); upload.bounds = fragment_bounds(upload.fragments); if (upload.fragments.empty()) { upload_index_remove(pack, upload); } else { upload_index_refresh(pack, upload); } } } const auto old_size = pack->uploads.size(); pack->uploads.erase( std::remove_if(pack->uploads.begin(), pack->uploads.end(), [](const Upload& upload) { return upload.fragments.empty(); }), pack->uploads.end()); erased_upload = old_size != pack->uploads.size(); /* Vector erasure can move surviving Upload objects, invalidating the * pointer map. Rebuild only on this uncommon path; ordinary primitive * invalidation updates only the few intersecting uploads. */ if (erased_upload) upload_index_rebuild(pack); } int hd_texture_pack_track_upload(HdTexturePack* pack, uint16_t x, uint16_t y, uint16_t width_words, uint16_t height, const uint16_t* words, size_t word_count, uint32_t* out_texture_hash) { if (out_texture_hash) *out_texture_hash = 0; const size_t required = size_t{width_words} * height; if (!pack || !words || width_words == 0 || height == 0 || width_words > kVramWidth || height > kVramHeight || word_count < required) return 0; const uint32_t hash = hd_texture_crc32_words_le(words, required); hd_texture_pack_invalidate(pack, x, y, width_words, height); Upload upload; upload.serial = pack->next_upload_serial++; upload.hash = hash; upload.width = width_words; upload.height = height; upload.fragments = physical_fragments(x, y, width_words, height); upload.bounds = fragment_bounds(upload.fragments); if (pack->upload_bounds.width == 0 || pack->upload_bounds.height == 0) { pack->upload_bounds = upload.bounds; } else { const unsigned right = std::max( pack->upload_bounds.x + pack->upload_bounds.width, upload.bounds.x + upload.bounds.width); const unsigned bottom = std::max( pack->upload_bounds.y + pack->upload_bounds.height, upload.bounds.y + upload.bounds.height); pack->upload_bounds.x = std::min(pack->upload_bounds.x, upload.bounds.x); pack->upload_bounds.y = std::min(pack->upload_bounds.y, upload.bounds.y); pack->upload_bounds.width = right - pack->upload_bounds.x; pack->upload_bounds.height = bottom - pack->upload_bounds.y; } pack->uploads.push_back(std::move(upload)); /* A push may reallocate the vector, so refresh all broad-phase pointers * after adding an upload. Upload creation is far less frequent than GPU * primitive submission. */ upload_index_rebuild(pack); if (out_texture_hash) *out_texture_hash = hash; return 1; } void hd_texture_pack_reset_tracking(HdTexturePack* pack) { if (pack) { pack->uploads.clear(); upload_index_clear(pack); pack->upload_bounds = Rect{}; } } int hd_texture_pack_tracking_state_save(const HdTexturePack* pack, uint8_t** out_data, size_t* out_size) { if (!out_data || !out_size) return 0; *out_data = nullptr; *out_size = 0; try { const uint64_t next_serial = pack ? pack->next_upload_serial : 1; const size_t upload_count = pack ? pack->uploads.size() : 0; if (next_serial == 0 || upload_count > kMaxTrackingStateUploads) return 0; size_t fragment_count = 0; if (pack) { for (const Upload& upload : pack->uploads) { if (upload.serial == 0 || upload.serial >= next_serial || upload.width == 0 || upload.height == 0 || upload.width > kVramWidth || upload.height > kVramHeight || upload.fragments.empty()) return 0; if (upload.fragments.size() > kMaxTrackingStateFragments - fragment_count) return 0; fragment_count += upload.fragments.size(); for (const Fragment& fragment : upload.fragments) if (!valid_fragment(upload, fragment)) return 0; } } if (upload_count > (std::numeric_limits::max() - kTrackingStateHeaderBytes) / kTrackingStateUploadBytes || fragment_count > (std::numeric_limits::max() - kTrackingStateHeaderBytes - upload_count * kTrackingStateUploadBytes) / kTrackingStateFragmentBytes) return 0; const size_t bytes = kTrackingStateHeaderBytes + upload_count * kTrackingStateUploadBytes + fragment_count * kTrackingStateFragmentBytes; if (bytes > kMaxTrackingStateBytes) return 0; std::vector wire; wire.reserve(bytes); append_u32_le(wire, kTrackingStateMagic); append_u32_le(wire, kTrackingStateVersion); append_u64_le(wire, next_serial); append_u32_le(wire, static_cast(upload_count)); append_u32_le(wire, 0); if (pack) { for (const Upload& upload : pack->uploads) { append_u64_le(wire, upload.serial); append_u32_le(wire, upload.hash); append_u16_le(wire, upload.width); append_u16_le(wire, upload.height); append_u32_le(wire, static_cast(upload.fragments.size())); for (const Fragment& fragment : upload.fragments) { append_u16_le(wire, static_cast(fragment.rect.x)); append_u16_le(wire, static_cast(fragment.rect.y)); append_u16_le(wire, static_cast(fragment.rect.width)); append_u16_le(wire, static_cast(fragment.rect.height)); append_u16_le(wire, static_cast(fragment.source_x)); append_u16_le(wire, static_cast(fragment.source_y)); } } } if (wire.size() != bytes) return 0; uint8_t* output = static_cast(std::malloc(bytes)); if (!output) return 0; std::memcpy(output, wire.data(), bytes); *out_data = output; *out_size = bytes; return 1; } catch (...) { return 0; } } int hd_texture_pack_tracking_state_check(const uint8_t* data, size_t size) { try { uint64_t next_serial = 0; return parse_tracking_state(data, size, &next_serial, nullptr) ? 1 : 0; } catch (...) { return 0; } } int hd_texture_pack_tracking_state_load(HdTexturePack* pack, const uint8_t* data, size_t size) { try { uint64_t next_serial = 0; std::vector uploads; if (!parse_tracking_state(data, size, &next_serial, &uploads)) return 0; if (pack) { pack->next_upload_serial = next_serial; pack->uploads.swap(uploads); upload_index_rebuild(pack); } return 1; } catch (...) { return 0; } } size_t hd_texture_pack_tracking_upload_count(const HdTexturePack* pack) { return pack ? pack->uploads.size() : 0; } int hd_texture_pack_match(HdTexturePack* pack, const HdTextureDrawQuery* query, HdTextureMatch* out_match) { if (out_match) std::memset(out_match, 0, sizeof(*out_match)); if (!pack || !query || query->depth > HD_TEXTURE_DEPTH_16BPP || !query->vram || query->vram_word_count < kVramWords) return HD_TEXTURE_LOOKUP_ERROR; const std::vector wanted = query_rectangles(*query); if (wanted.empty()) return HD_TEXTURE_LOOKUP_ERROR; const uint32_t palette_hash = hd_texture_hash_clut( query->vram, query->vram_word_count, query->clut_x, query->clut_y, query->depth); std::vector candidate_serials; upload_index_collect(pack, wanted, &candidate_serials); const Upload* candidate = nullptr; const EntryRecord* candidate_entry = nullptr; for (const uint64_t serial : candidate_serials) { const auto indexed = pack->upload_by_serial.find(serial); if (indexed == pack->upload_by_serial.end() || !indexed->second) continue; const Upload& upload = *indexed->second; const auto record = pack->entries.find(make_key(upload.hash, palette_hash)); if (record == pack->entries.end()) continue; if (!covered_by_upload(upload, wanted)) continue; if (record->second.ambiguous) return HD_TEXTURE_LOOKUP_AMBIGUOUS; if (candidate) return HD_TEXTURE_LOOKUP_AMBIGUOUS; candidate = &upload; candidate_entry = &record->second; } if (!candidate || !candidate_entry) return HD_TEXTURE_LOOKUP_NONE; if (out_match) { fill_entry(*candidate_entry, &out_match->entry); out_match->upload_serial = candidate->serial; out_match->upload_width_words = candidate->width; out_match->upload_height = candidate->height; const unsigned pixels_per_word = query->depth == HD_TEXTURE_DEPTH_4BPP ? 4u : query->depth == HD_TEXTURE_DEPTH_8BPP ? 2u : 1u; const unsigned first_x = (unsigned{query->page_x} + query->u_first / pixels_per_word) & (kVramWidth - 1); const unsigned first_y = (unsigned{query->page_y} + query->v_first) & (kVramHeight - 1); for (const Fragment& fragment : candidate->fragments) { if (contains_point(fragment.rect, first_x, first_y)) { out_match->source_word_x = static_cast( fragment.source_x + first_x - fragment.rect.x); out_match->source_y = static_cast( fragment.source_y + first_y - fragment.rect.y); break; } } } return HD_TEXTURE_LOOKUP_FOUND; } int hd_texture_pack_match_draw(HdTexturePack* pack, uint16_t texpage, uint16_t clut_x, uint16_t clut_y, uint8_t u_first, uint8_t u_last, uint8_t v_first, uint8_t v_last, const uint16_t* vram, size_t vram_word_count, HdTextureMatch* out_match) { const uint8_t depth = static_cast((texpage >> 7) & 3u); if (depth > HD_TEXTURE_DEPTH_16BPP) { if (out_match) std::memset(out_match, 0, sizeof(*out_match)); return HD_TEXTURE_LOOKUP_ERROR; } HdTextureDrawQuery query{}; query.page_x = static_cast((texpage & 0xFu) * 64u); query.page_y = static_cast(((texpage >> 4) & 1u) * 256u); query.depth = depth; query.u_first = u_first; query.u_last = u_last; query.v_first = v_first; query.v_last = v_last; query.clut_x = clut_x; query.clut_y = clut_y; query.vram = vram; query.vram_word_count = vram_word_count; return hd_texture_pack_match(pack, &query, out_match); } void hd_texture_pack_set_decode_budget(HdTexturePack* pack, size_t budget_bytes) { if (!pack) return; std::lock_guard lock(pack->decode.mutex); pack->decode.budget = budget_bytes; evict_decode_cache(pack->decode); } int hd_texture_pack_request_decode(HdTexturePack* pack, uint32_t texture_hash, uint32_t palette_hash) { #ifdef HD_TEXTURE_PACK_DISABLE_PNG_DECODE (void)pack; (void)texture_hash; (void)palette_hash; return HD_TEXTURE_LOOKUP_ERROR; #else if (!pack) return HD_TEXTURE_LOOKUP_ERROR; const uint64_t key = make_key(texture_hash, palette_hash); const auto record = pack->entries.find(key); if (record == pack->entries.end() || record->second.ambiguous) return HD_TEXTURE_LOOKUP_ERROR; std::lock_guard lock(pack->decode.mutex); auto found = pack->decode.items.find(key); if (found != pack->decode.items.end()) { if (found->second.state == DecodeState::Ready) { found->second.last_used = ++pack->decode.tick; return HD_TEXTURE_LOOKUP_FOUND; } return found->second.state == DecodeState::Failed ? HD_TEXTURE_LOOKUP_ERROR : HD_TEXTURE_LOOKUP_NONE; } if (pack->decode.budget == 0) return HD_TEXTURE_LOOKUP_ERROR; if (pack->decode.queue.size() >= kMaxDecodeQueue) return HD_TEXTURE_LOOKUP_NONE; DecodeItem item; item.path = record->second.replacement_path; pack->decode.items.emplace(key, std::move(item)); pack->decode.queue.push_back(key); if (!pack->decode.started) { pack->decode.started = true; pack->decode.worker = std::thread(decode_worker, &pack->decode); } pack->decode.wake.notify_one(); return HD_TEXTURE_LOOKUP_NONE; #endif } int hd_texture_pack_acquire_decoded(HdTexturePack* pack, uint32_t texture_hash, uint32_t palette_hash, HdTexturePixels* out_pixels) { if (out_pixels) std::memset(out_pixels, 0, sizeof(*out_pixels)); if (!pack || !out_pixels) return HD_TEXTURE_LOOKUP_ERROR; std::lock_guard lock(pack->decode.mutex); const auto found = pack->decode.items.find(make_key(texture_hash, palette_hash)); if (found == pack->decode.items.end()) return HD_TEXTURE_LOOKUP_NONE; if (found->second.state == DecodeState::Failed) return HD_TEXTURE_LOOKUP_ERROR; if (found->second.state != DecodeState::Ready || !found->second.image) return HD_TEXTURE_LOOKUP_NONE; PixelLease* lease = new (std::nothrow) PixelLease{found->second.image}; if (!lease) return HD_TEXTURE_LOOKUP_ERROR; found->second.last_used = ++pack->decode.tick; out_pixels->rgba = lease->image->rgba.data(); out_pixels->width = lease->image->width; out_pixels->height = lease->image->height; out_pixels->stride = lease->image->width * 4; out_pixels->lease = lease; return HD_TEXTURE_LOOKUP_FOUND; } void hd_texture_pixels_release(HdTexturePixels* pixels) { if (!pixels) return; delete static_cast(pixels->lease); std::memset(pixels, 0, sizeof(*pixels)); } } // extern "C"