mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
f7fed074f1
- Filtered replacement sampling: with any texture filter but nearest, the
replacement is sampled through its mip chain (textureGrad), the footprint
clamped to the primitive's own UV bounds so atlas neighbours cannot bleed
in. Texture windows (repeat) and UVs outside the page keep hd_texel.
- Edge clamp: an out-of-bounds sample clamped to the primitive's UV bounds
takes the edge's sub-texel position too (keeping the outside fraction read
a 1 px sliver of the next glyph at sprite borders).
- GL cache: 4096 slots and a PSX_HD_GL_CACHE_MIB budget (default 2048 MiB)
with a direct-mapped hint table; a 4x pack of one race scene is several
hundred textures, and the old 256 slot / 128 MiB cache re-uploaded and
re-mipped them every frame.
- Invalidation: duck_texture_pack_invalidate returns early when a native
write touches no residency or cache entry (almost every draw under HD
authority lands on the frame buffer), and retire_unused walks the
residency lists once instead of once per source.
- Indexed replacements: a TTTTTTTT-index.png key resolves through the palette
in use at draw time, for packs whose textures are shared by several CLUTs.
- Debug: TCP {"cmd":"hd_uploads","x":..,"y":..} lists the texture uploads
covering a VRAM point (pack authoring).
Measured with a full HD HUD and course pack for R4: steady 60 fps in races
where the old cache dropped to the high 20s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rkyma8rbyPiccs9n2bkyG6
1389 lines
55 KiB
C++
1389 lines
55 KiB
C++
#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 <algorithm>
|
|
#include <array>
|
|
#include <condition_variable>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <new>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#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<char>(std::tolower(static_cast<unsigned char>(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;
|
|
if (ascii_lower(text.substr(dash + 1)) == "index") {
|
|
*palette_hash = HD_TEXTURE_PALETTE_INDEXED;
|
|
return parse_hex_component(text.substr(0, dash), texture_hash);
|
|
}
|
|
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<uint32_t, 256>& crc_table() {
|
|
static const std::array<uint32_t, 256> table = [] {
|
|
std::array<uint32_t, 256> 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<Fragment> physical_fragments(unsigned x,
|
|
unsigned y,
|
|
unsigned width,
|
|
unsigned height) {
|
|
std::vector<Fragment> 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<std::pair<unsigned, unsigned>, 2> xs{{
|
|
{px, first_w}, {0, width - first_w}
|
|
}};
|
|
const std::array<std::pair<unsigned, unsigned>, 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<Rect> physical_rects(unsigned x,
|
|
unsigned y,
|
|
unsigned width,
|
|
unsigned height) {
|
|
std::vector<Rect> result;
|
|
for (const Fragment& fragment : physical_fragments(x, y, width, height))
|
|
result.push_back(fragment.rect);
|
|
return result;
|
|
}
|
|
|
|
std::vector<Fragment> 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<Fragment> 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<Rect> subtract_rect(const Rect& original, const Rect& cut) {
|
|
const Fragment source{original, 0, 0};
|
|
std::vector<Rect> 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<Fragment> fragments;
|
|
/* Runtime-only broad-phase membership; never serialized. */
|
|
std::vector<uint16_t> index_tiles;
|
|
};
|
|
|
|
Rect fragment_bounds(const std::vector<Fragment>& 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<uint8_t>& output, uint16_t value) {
|
|
output.push_back(static_cast<uint8_t>(value));
|
|
output.push_back(static_cast<uint8_t>(value >> 8));
|
|
}
|
|
|
|
void append_u32_le(std::vector<uint8_t>& output, uint32_t value) {
|
|
append_u16_le(output, static_cast<uint16_t>(value));
|
|
append_u16_le(output, static_cast<uint16_t>(value >> 16));
|
|
}
|
|
|
|
void append_u64_le(std::vector<uint8_t>& output, uint64_t value) {
|
|
append_u32_le(output, static_cast<uint32_t>(value));
|
|
append_u32_le(output, static_cast<uint32_t>(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<uint16_t>(p[0]) |
|
|
static_cast<uint16_t>(static_cast<uint16_t>(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<uint32_t>(low) |
|
|
(static_cast<uint32_t>(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<uint64_t>(low) |
|
|
(static_cast<uint64_t>(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<Upload>* 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<Upload> parsed;
|
|
if (out_uploads) parsed.reserve(upload_count);
|
|
std::vector<uint64_t> 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<Rect>& query) {
|
|
for (const Rect& wanted : query) {
|
|
std::vector<Rect> uncovered{wanted};
|
|
for (const Fragment& fragment : upload.fragments) {
|
|
std::vector<Rect> next;
|
|
for (const Rect& piece : uncovered) {
|
|
std::vector<Rect> 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<uint8_t> 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<DecodedImage> image;
|
|
size_t bytes = 0;
|
|
uint64_t last_used = 0;
|
|
};
|
|
|
|
struct DecodeCache {
|
|
std::mutex mutex;
|
|
std::condition_variable wake;
|
|
std::unordered_map<uint64_t, DecodeItem> items;
|
|
std::deque<uint64_t> 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<std::mutex> lock(mutex);
|
|
stop = true;
|
|
}
|
|
wake.notify_all();
|
|
if (worker.joinable()) worker.join();
|
|
}
|
|
};
|
|
|
|
struct PixelLease {
|
|
std::shared_ptr<DecodedImage> 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<std::mutex> 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<uint8_t> encoded;
|
|
{
|
|
std::ifstream input(path, std::ios::binary | std::ios::ate);
|
|
std::streamoff length = -1;
|
|
if (input) length = static_cast<std::streamoff>(input.tellg());
|
|
if (length > 0 && static_cast<uint64_t>(length) <= kMaxPngFileBytes &&
|
|
static_cast<uint64_t>(length) <=
|
|
static_cast<uint64_t>(std::numeric_limits<int>::max())) {
|
|
encoded.resize(static_cast<size_t>(length));
|
|
input.seekg(0);
|
|
if (!input.read(reinterpret_cast<char*>(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<int>(encoded.size()),
|
|
&width, &height, &components, 4);
|
|
std::shared_ptr<DecodedImage> decoded;
|
|
size_t bytes = 0;
|
|
if (loaded && width > 0 && height > 0 &&
|
|
static_cast<uint64_t>(width) * static_cast<uint64_t>(height) <=
|
|
std::numeric_limits<size_t>::max() / 4) {
|
|
bytes = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
|
|
decoded = std::make_shared<DecodedImage>();
|
|
decoded->width = static_cast<uint32_t>(width);
|
|
decoded->height = static_cast<uint32_t>(height);
|
|
decoded->rgba.assign(loaded, loaded + bytes);
|
|
}
|
|
if (loaded) stbi_image_free(loaded);
|
|
|
|
std::lock_guard<std::mutex> 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<uint64_t, std::string>* 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<Rect> 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<std::pair<unsigned, unsigned>> u_ranges;
|
|
std::vector<std::pair<unsigned, unsigned>> 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<Rect> 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<Rect> 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<uint64_t, EntryRecord> entries;
|
|
std::unordered_set<uint32_t> 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<Upload> uploads;
|
|
std::array<std::vector<uint64_t>, kUploadIndexCells> upload_index;
|
|
std::unordered_map<uint64_t, Upload*> 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<uint16_t>(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<Rect>& query,
|
|
std::vector<uint64_t>* 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<fs::path> 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<HdTexturePack>();
|
|
pack->asset_root = input.string();
|
|
pack->replacement_root = replacement.lexically_normal().string();
|
|
|
|
std::unordered_map<uint64_t, std::string> mappings;
|
|
parse_hashes_ini(hashes_ini, &mappings);
|
|
pack->logical_mapping_count = mappings.size();
|
|
|
|
std::vector<fs::path> 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<uint32_t>(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<uint64_t> 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<uint8_t>(words[i] & 0xFFu));
|
|
crc = crc_byte(crc, static_cast<uint8_t>(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<uint16_t, 256> 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<Rect> cuts = physical_rects(x, y, width_words, height);
|
|
std::vector<uint64_t> 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<Fragment> 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<Fragment> 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;
|
|
}
|
|
|
|
/* Debug: uploads whose live fragments cover VRAM (x, y), newest first, as
|
|
* JSON objects: serial, texture hash, size, fragment rect and source offset,
|
|
* and whether the pack holds any replacement for that hash. */
|
|
int hd_texture_pack_debug_uploads_at(const HdTexturePack* pack, unsigned x, unsigned y,
|
|
char* out, size_t cap) {
|
|
if (!out || cap == 0) return 0;
|
|
size_t pos = 0;
|
|
int n = 0;
|
|
out[0] = ' |