Files
psxrecomp/runtime/src/gpu_hd_textures.cpp
T
Matt Stanley f9db2561d0 Fix persistent HD texture cache churn and missing live background dumps (#626)
* fix: reuse resident HD textures and publish live background dumps

* test: keep residency stubs local to the filtering fixture
2026-10-10 12:55:54 -07:00

636 lines
35 KiB
C++

#include "gpu_hd_textures.h"
#include "gpu_gl_renderer.h"
#include "hd_texture_pack.h"
#include "duckstation_texture_pack.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <filesystem>
#include <memory>
#include <new>
#include <string>
#include <vector>
namespace {
constexpr size_t kVramWords = 1024u * 512u;
constexpr size_t kCompositionBudget = 64u * 1024u * 1024u;
/* A tiled scene can use dozens of small compositions per frame. The byte
* budget bounds memory; a sixteen-entry limit thrashed below that budget. */
constexpr size_t kCompositionCount = 256;
constexpr uint32_t kCompositionDimension = 8192;
constexpr size_t kMaxParts = 64;
struct Composition {
std::vector<uint64_t> signature;
std::vector<uint8_t> rgba;
uint32_t width = 0, height = 0;
uint64_t cache_key = 0, last_use = 0;
uint64_t ready_mask = 0;
};
struct Session {
std::string root;
HdTexturePack* beetle = nullptr;
DuckTexturePack* duck = nullptr;
bool replacements = false;
bool dump = false;
bool dump_failed = false;
bool copy_pending = false;
std::vector<std::shared_ptr<Composition>> compositions;
size_t composition_bytes = 0;
uint64_t composition_clock = 0;
std::string dump_error;
std::deque<std::string> diagnostics;
/* Rectangles of uploads whose CRC keys a Beetle replacement, kept so a
* restage can re-verify them against the restored VRAM (bounded). */
std::vector<std::array<uint16_t, 4>> learned;
GpuHdTextureDiag diag{};
~Session() { hd_texture_pack_destroy(beetle); duck_texture_pack_destroy(duck); }
};
struct Lease {
HdTexturePixels beetle{};
DuckTexturePixels duck{};
std::shared_ptr<Composition> composition;
~Lease() { hd_texture_pixels_release(&beetle); duck_texture_pixels_release(&duck); }
};
std::unique_ptr<Session> session;
const uint16_t* native_vram = nullptr;
uint64_t generation = 1;
uint64_t composition_serial = 0;
struct DecodedParts {
std::array<DuckTexturePixels, kMaxParts> pixels{};
~DecodedParts() { for (auto& pixel : pixels) duck_texture_pixels_release(&pixel); }
};
void fail(char* error, size_t capacity, const char* message) {
if (error && capacity) std::snprintf(error, capacity, "%s", message);
}
bool is_beetle_root(const std::filesystem::path& root) {
std::error_code error;
if (std::filesystem::exists(root / "Hashes.ini", error)) return true;
if (std::filesystem::exists(root.parent_path() / "Hashes.ini", error)) return true;
const auto name = root.filename().u8string();
const std::string suffix = "-texture-replacements";
return name.size() >= suffix.size() &&
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0;
}
int window_coord(int value, int mask, int offset) {
return (value & ~(mask * 8)) | ((offset & mask) * 8);
}
bool make_query(uint16_t texpage, uint16_t cx, uint16_t cy, const int limits[4],
uint32_t window, HdTextureDrawQuery& query) {
if (!native_vram || !limits || ((texpage >> 7) & 3u) == 3u) return false;
if (limits[0] < 0 || limits[1] < 0 || limits[2] > 255 || limits[3] > 255 ||
limits[0] > limits[2] || limits[1] > limits[3]) return false;
int first[2] = {255,255}, last[2] = {0,0};
for (int axis = 0; axis < 2; ++axis) {
int mask = (window >> (axis * 5)) & 31;
int offset = (window >> (10 + axis * 5)) & 31;
for (int p = limits[axis]; p <= limits[axis + 2]; ++p) {
const int mapped = window_coord(p, mask, offset);
first[axis] = std::min(first[axis], mapped);
last[axis] = std::max(last[axis], mapped);
}
}
query = {};
query.page_x = (texpage & 15u) * 64u;
query.page_y = ((texpage >> 4) & 1u) * 256u;
query.depth = (texpage >> 7) & 3u;
query.u_first = static_cast<uint8_t>(first[0]);
query.u_last = static_cast<uint8_t>(last[0]);
query.v_first = static_cast<uint8_t>(first[1]);
query.v_last = static_cast<uint8_t>(last[1]);
query.clut_x = cx; query.clut_y = cy;
query.vram = native_vram; query.vram_word_count = kVramWords;
return true;
}
uint8_t replacement_class(const uint8_t* pixel, bool semi) {
if (!semi) return pixel[3] >= 128 ? 255 : 0;
if (!(pixel[0] | pixel[1] | pixel[2] | pixel[3])) return 0;
if (pixel[3] <= 242) return 128;
return (pixel[0] | pixel[1] | pixel[2]) ? 255 : 0;
}
void replacement_pixel(const DuckTexturePixels& image, bool semi, bool linear,
double x, double y, uint8_t* output) {
const int nx = std::clamp(static_cast<int>(std::floor(x)), 0, static_cast<int>(image.width) - 1);
const int ny = std::clamp(static_cast<int>(std::floor(y)), 0, static_cast<int>(image.height) - 1);
const uint8_t* nearest = image.rgba + static_cast<size_t>(ny) * image.stride + nx * 4;
const uint8_t alpha = replacement_class(nearest, semi);
output[3] = alpha;
if (!alpha) { output[0] = output[1] = output[2] = 0; return; }
if (!linear) { std::memcpy(output, nearest, 3); return; }
/* Scaling filters colors within the nearest PSX class. Alpha class is
* discrete: mixing cutout/STP/opaque would alter the primitive's blend. */
x -= 0.5; y -= 0.5;
const int bx = static_cast<int>(std::floor(x)), by = static_cast<int>(std::floor(y));
const double fx = x - bx, fy = y - by;
double sum[3]{}, weight = 0;
for (int dy = 0; dy < 2; ++dy) for (int dx = 0; dx < 2; ++dx) {
const int px = std::clamp(bx + dx, 0, static_cast<int>(image.width) - 1);
const int py = std::clamp(by + dy, 0, static_cast<int>(image.height) - 1);
const uint8_t* pixel = image.rgba + static_cast<size_t>(py) * image.stride + px * 4;
if (replacement_class(pixel, semi) != alpha) continue;
const double w = (dx ? fx : 1 - fx) * (dy ? fy : 1 - fy);
for (int channel = 0; channel < 3; ++channel) sum[channel] += pixel[channel] * w;
weight += w;
}
for (int channel = 0; channel < 3; ++channel)
output[channel] = weight > 0 ? static_cast<uint8_t>(std::clamp(sum[channel] / weight + 0.5, 0.0, 255.0)) : nearest[channel];
}
void native_pixel(const HdTextureDrawQuery& query, const std::array<uint16_t,256>& palette,
int u, int v, uint8_t* output) {
const unsigned per_word = 4u >> query.depth;
uint16_t word = query.vram[(query.page_y + v) * 1024u + query.page_x + u / per_word];
if (query.depth == HD_TEXTURE_DEPTH_4BPP) word = palette[(word >> ((u & 3) * 4)) & 15];
else if (query.depth == HD_TEXTURE_DEPTH_8BPP) word = palette[(word >> ((u & 1) * 8)) & 255];
output[0] = static_cast<uint8_t>(((word & 31u) * 255u + 15u) / 31u);
output[1] = static_cast<uint8_t>((((word >> 5) & 31u) * 255u + 15u) / 31u);
output[2] = static_cast<uint8_t>((((word >> 10) & 31u) * 255u + 15u) / 31u);
output[3] = !word ? 0 : (word & 0x8000u) ? 128 : 255;
}
std::vector<uint64_t> composition_signature(const HdTextureDrawQuery& query,
const std::array<DuckTextureMatch,kMaxParts>& matches, size_t count,
std::array<uint16_t,256>& palette) {
const unsigned per_word = 4u >> query.depth;
const unsigned first_word = query.u_first / per_word, last_word = query.u_last / per_word;
const uint32_t native_height = query.v_last - query.v_first + 1;
const size_t palette_size = query.depth == HD_TEXTURE_DEPTH_4BPP ? 16 : query.depth == HD_TEXTURE_DEPTH_8BPP ? 256 : 0;
for (size_t i = 0; i < palette_size; ++i)
palette[i] = query.vram[(query.clut_y & 511u) * 1024u + ((query.clut_x + i) & 1023u)];
std::vector<uint64_t> signature{
static_cast<uint64_t>(query.page_x) | (static_cast<uint64_t>(query.page_y) << 16) | (static_cast<uint64_t>(query.depth) << 32),
static_cast<uint64_t>(query.u_first) | (static_cast<uint64_t>(query.v_first) << 8) |
(static_cast<uint64_t>(query.u_last) << 16) | (static_cast<uint64_t>(query.v_last) << 24),
duck_texture_hash_rect(query.vram, query.vram_word_count, query.page_x + first_word,
query.page_y + query.v_first, last_word - first_word + 1, native_height),
duck_texture_hash_words_le(palette.data(), palette_size)};
signature.push_back(duck_texture_pack_linear_filter(session->duck) != 0);
for (size_t i = 0; i < count; ++i) {
const auto& match = matches[i];
signature.insert(signature.end(), {match.entry_id, static_cast<uint32_t>(match.origin_u),
static_cast<uint32_t>(match.origin_v), match.source_width, match.source_height,
static_cast<uint32_t>(match.clip_u), static_cast<uint32_t>(match.clip_v),
match.clip_width, match.clip_height, match.key.semitransparent});
}
return signature;
}
std::shared_ptr<Composition> find_composition(const std::vector<uint64_t>& signature, uint64_t ready_mask) {
for (auto& cached : session->compositions)
if (cached->ready_mask == ready_mask && cached->signature == signature) {
++session->diag.composition_hits;
cached->last_use = ++session->composition_clock; return cached;
}
return {};
}
std::shared_ptr<Composition> compose(const HdTextureDrawQuery& query,
const std::array<DuckTextureMatch,kMaxParts>& matches, const DecodedParts& decoded, size_t count,
std::vector<uint64_t> signature, const std::array<uint16_t,256>& palette) {
const unsigned per_word = 4u >> query.depth;
if (query.page_x + query.u_last / per_word >= 1024 || query.page_y + query.v_last >= 512) return {};
const uint32_t native_width = query.u_last - query.u_first + 1;
const uint32_t native_height = query.v_last - query.v_first + 1;
const bool linear = duck_texture_pack_linear_filter(session->duck) != 0;
double scale_x = 1, scale_y = 1;
uint64_t ready_mask = 0;
for (size_t i = 0; i < count; ++i) {
const auto& pixel = decoded.pixels[i];
if (!pixel.rgba) continue;
ready_mask |= UINT64_C(1) << i;
scale_x = std::max(scale_x, static_cast<double>(pixel.width) / matches[i].source_width);
scale_y = std::max(scale_y, static_cast<double>(pixel.height) / matches[i].source_height);
}
if (auto cached = find_composition(signature, ready_mask)) return cached;
const double wanted_width = std::ceil(native_width * scale_x);
const double wanted_height = std::ceil(native_height * scale_y);
if (wanted_width > kCompositionDimension || wanted_height > kCompositionDimension) return {};
const uint32_t width = static_cast<uint32_t>(wanted_width), height = static_cast<uint32_t>(wanted_height);
const size_t bytes = static_cast<size_t>(width) * height * 4u;
if (bytes > kCompositionBudget) return {};
while (!session->compositions.empty() && (session->compositions.size() >= kCompositionCount ||
session->composition_bytes + bytes > kCompositionBudget)) {
auto oldest = std::min_element(session->compositions.begin(), session->compositions.end(),
[](const auto& a, const auto& b) { return a->last_use < b->last_use; });
session->composition_bytes -= (*oldest)->rgba.size(); session->compositions.erase(oldest);
}
auto result = std::make_shared<Composition>();
++session->diag.composition_builds;
session->diag.composition_pixels += static_cast<uint64_t>(width) * height;
result->signature = std::move(signature); result->width = width; result->height = height;
result->ready_mask = ready_mask;
result->rgba.resize(bytes);
for (uint32_t y = 0; y < height; ++y) for (uint32_t x = 0; x < width; ++x)
native_pixel(query, palette, query.u_first + (static_cast<uint64_t>(x) * 2 + 1) * native_width / (width * 2u),
query.v_first + (static_cast<uint64_t>(y) * 2 + 1) * native_height / (height * 2u),
result->rgba.data() + (static_cast<size_t>(y) * width + x) * 4);
for (size_t i = 0; i < count; ++i) {
const auto& image = decoded.pixels[i]; const auto& match = matches[i];
if (!image.rgba) continue;
const int x0 = std::clamp(static_cast<int>(std::floor((match.clip_u - query.u_first) * static_cast<double>(width) / native_width)), 0, static_cast<int>(width));
const int y0 = std::clamp(static_cast<int>(std::floor((match.clip_v - query.v_first) * static_cast<double>(height) / native_height)), 0, static_cast<int>(height));
const int x1 = std::clamp(static_cast<int>(std::ceil((match.clip_u + match.clip_width - query.u_first) * static_cast<double>(width) / native_width)), 0, static_cast<int>(width));
const int y1 = std::clamp(static_cast<int>(std::ceil((match.clip_v + match.clip_height - query.v_first) * static_cast<double>(height) / native_height)), 0, static_cast<int>(height));
for (int y = y0; y < y1; ++y) for (int x = x0; x < x1; ++x) {
const double u = query.u_first + (x + 0.5) * native_width / width;
const double v = query.v_first + (y + 0.5) * native_height / height;
if (u < match.clip_u || u >= match.clip_u + match.clip_width ||
v < match.clip_v || v >= match.clip_v + match.clip_height) continue;
replacement_pixel(image, match.key.semitransparent != 0, linear,
(u - match.origin_u) * image.width / match.source_width,
(v - match.origin_v) * image.height / match.source_height,
result->rgba.data() + (static_cast<size_t>(y) * width + x) * 4);
}
}
result->cache_key = (UINT64_C(1) << 63) | ++composition_serial;
result->last_use = ++session->composition_clock;
session->compositions.push_back(result); session->composition_bytes += bytes;
return result;
}
bool dumps_suppressed = false;
void dump_query(const HdTextureDrawQuery& query, int semi) {
if (!session->dump || session->dump_failed || dumps_suppressed) return;
char error[256]{};
const int status = duck_texture_pack_dump_draw(session->duck, &query, semi, error, sizeof(error));
if (status == HD_TEXTURE_LOOKUP_FOUND) ++session->diag.dumped_textures;
if (status < 0) {
/* Keep the configured raster authority until a safe session boundary.
* Tearing it down inside a pre-draw query would lose command order. */
session->dump_failed = true;
session->dump_error = error;
}
}
}
extern "C" int gpu_hd_textures_configure(const char* root, int replacements,
int dump, char* error, size_t capacity) {
if (error && capacity) error[0] = 0;
if ((!root || !root[0]) && !replacements && !dump) {
gpu_hd_textures_shutdown(); return 1;
}
if (!root || !root[0]) { fail(error, capacity, "Select an external texture-pack directory."); return 0; }
try {
auto next = std::make_unique<Session>();
next->root = root; next->replacements = replacements != 0; next->dump = dump != 0;
const bool beetle = is_beetle_root(std::filesystem::u8path(root));
if (beetle) {
if (!hd_texture_pack_create(root, &next->beetle, error, capacity)) return 0;
} else {
char ignored[256]{};
hd_texture_pack_create(root, &next->beetle, ignored, sizeof(ignored));
}
if (!next->beetle) {
auto directory = std::filesystem::u8path(root);
if (directory.filename().u8string() == "replacements") directory = directory.parent_path();
std::filesystem::create_directories(directory / "replacements");
std::filesystem::create_directories(directory / "dumps");
}
if (!duck_texture_pack_create(root, &next->duck, error, capacity)) return 0;
/* Quiesce renderer users (GL render thread: dump_draw appends to the
* old session's sources) before flushing its dumps or reading its
* residency; the cache is rebuilt from the new session. */
if (session) gl_renderer_clear_hd_texture_cache();
if (session) {
char dump_error[256]{};
if (duck_texture_pack_flush_dumps(session->duck, dump_error, sizeof(dump_error)) < 0) {
session->dump_error = dump_error;
fail(error, capacity, dump_error); return 0;
}
}
if (session && (session->replacements || session->dump) && session->root == next->root) {
if (!duck_texture_pack_copy_tracking(next->duck, session->duck)) {
fail(error, capacity, "Could not preserve texture-upload tracking during reload."); return 0;
}
if (session->beetle && next->beetle) {
uint8_t* saved = nullptr; size_t saved_size = 0;
if (!hd_texture_pack_tracking_state_save(session->beetle, &saved, &saved_size)) {
fail(error, capacity, "Could not save Beetle texture-upload tracking during reload."); return 0;
}
const int restored = hd_texture_pack_tracking_state_load(next->beetle, saved, saved_size);
std::free(saved);
if (!restored) { fail(error, capacity, "Could not restore Beetle texture-upload tracking during reload."); return 0; }
/* Restage after rewind/load rehashes these bounded upload
* rectangles against restored VRAM. A same-pack reload must
* preserve that knowledge as well as current residency. */
next->learned = session->learned;
}
}
/* This also flushes all queued users before old replacement textures
* and pixel leases may be destroyed. */
gl_renderer_set_hd_texture_mode(next->replacements || next->dump);
gl_renderer_clear_hd_texture_cache();
session = std::move(next); ++generation;
DuckTexturePackInfo info{};
duck_texture_pack_get_info(session->duck, &info);
size_t replacements_found = info.replacement_count;
size_t ambiguous = info.ambiguous_count;
if (session->beetle) {
HdTexturePackInfo beetle_info{};
hd_texture_pack_get_info(session->beetle, &beetle_info);
replacements_found = beetle_info.unique_key_count;
ambiguous = beetle_info.ambiguous_key_count;
}
std::fprintf(stdout, "psxrecomp: texture pack '%s': %zu replacements, %zu ambiguous, %zu ignored\n",
session->root.c_str(), replacements_found, ambiguous, info.ignored_count);
if (info.diagnostic && info.diagnostic[0])
std::fprintf(stderr, "psxrecomp: texture pack: %s\n", info.diagnostic);
return 1;
} catch (const std::exception& exception) {
fail(error, capacity, exception.what()); return 0;
}
}
extern "C" void gpu_hd_textures_shutdown(void) {
gl_renderer_set_hd_texture_mode(0);
gl_renderer_clear_hd_texture_cache();
session.reset(); ++generation;
}
extern "C" int gpu_hd_textures_reload(char* error, size_t capacity) {
if (!session) { fail(error, capacity, "No texture-pack session is active."); return 0; }
const std::string root = session->root;
return gpu_hd_textures_configure(root.c_str(), session->replacements, session->dump, error, capacity);
}
extern "C" void gpu_hd_textures_set_dump_enabled(int enabled) {
if (!session || session->dump == (enabled != 0)) return;
/* The render thread may be dumping (dump_draw) or tracking residency. */
gl_renderer_render_thread_sync("hd_set_dump");
if (enabled && !session->replacements) gpu_hd_textures_reset_tracking();
if (!enabled) {
char error[256]{};
if (duck_texture_pack_flush_dumps(session->duck, error, sizeof(error)) < 0) {
session->dump_error = error; session->dump_failed = true; return;
}
}
gl_renderer_set_hd_texture_mode(session->replacements || enabled);
session->dump = enabled != 0;
session->dump_failed = false;
}
extern "C" int gpu_hd_textures_replacements_enabled(void) { return session && session->replacements; }
extern "C" int gpu_hd_textures_dump_enabled(void) { return session && session->dump; }
extern "C" int gpu_hd_textures_active(void) { return session && (session->replacements || session->dump); }
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) {
if (!out) return;
*out = {};
if (!session) { out->root = ""; out->diagnostic = ""; return; }
/* Counters and dump queues are written by the GL render thread. */
gl_renderer_render_thread_sync("hd_diag");
*out = session->diag;
out->root = session->root.c_str(); out->active = gpu_hd_textures_active();
out->replacements = session->replacements; out->dump = session->dump && !session->dump_failed;
out->format = session->beetle ? 1 : 2;
DuckTexturePackInfo info{}; duck_texture_pack_get_info(session->duck, &info);
out->decoded_images = info.decoded_images;
out->decode_evictions = info.decode_evictions;
out->decoded_bytes = info.decoded_bytes;
out->dumped_textures = info.queued_dump_count; out->pending_dump_sources = info.pending_dump_sources;
if (session->beetle) { HdTexturePackInfo beetle_info{}; hd_texture_pack_get_info(session->beetle, &beetle_info); out->replacement_count = beetle_info.unique_key_count; }
else out->replacement_count = info.replacement_count;
std::string diagnostic = info.diagnostic ? info.diagnostic : "";
if (!session->dump_error.empty()) {
if (!diagnostic.empty()) diagnostic += "; ";
diagnostic += session->dump_error;
}
if (session->diagnostics.empty() || session->diagnostics.back() != diagnostic)
session->diagnostics.push_back(std::move(diagnostic));
out->diagnostic = session->diagnostics.back().c_str();
}
extern "C" void gpu_hd_textures_note_applied(void) { if (session) ++session->diag.applied_draws; }
extern "C" void gpu_hd_textures_note_gl_cache(int eviction) {
if (session) {
if (eviction) ++session->diag.gl_evictions;
else ++session->diag.gl_uploads;
}
}
extern "C" void gpu_hd_textures_bind_vram(const uint16_t* vram) { native_vram = vram; }
extern "C" void gpu_hd_textures_suppress_dumps(int on) { dumps_suppressed = on != 0; }
extern "C" void gpu_hd_textures_set_vram(const uint16_t* vram) { native_vram = vram; gpu_hd_textures_reset_tracking(); }
extern "C" void gpu_hd_textures_reset_tracking(void) {
if (!session) return;
gl_renderer_clear_hd_texture_cache();
session->compositions.clear(); session->composition_bytes = 0;
session->copy_pending = false;
hd_texture_pack_reset_tracking(session->beetle);
duck_texture_pack_reset_tracking(session->duck);
}
extern "C" void gpu_hd_textures_begin_upload(int x, int y, int width, int height) {
/* The GP0 header arrives before native payload writes. Complete uploads
* establish their fresh provenance only in track_upload(), after writing. */
gpu_hd_textures_invalidate(x, y, width, height);
}
extern "C" void gpu_hd_textures_begin_copy(int sx, int sy, int dx, int dy, int width, int height) {
if (!session || !native_vram) return;
session->copy_pending = false;
if (width < 1 || width > 1024 || height < 1 || height > 512) { gpu_hd_textures_reset_tracking(); return; }
hd_texture_pack_invalidate(session->beetle, dx & 1023, dy & 511, width, height);
session->copy_pending = duck_texture_pack_begin_copy(session->duck, sx & 1023, sy & 511,
dx & 1023, dy & 511, width, height, native_vram, kVramWords) > 0;
if (!session->copy_pending)
duck_texture_pack_invalidate(session->duck, dx & 1023, dy & 511, width, height);
}
extern "C" void gpu_hd_textures_end_copy(void) {
if (!session || !session->copy_pending) return;
session->copy_pending = false;
if (duck_texture_pack_end_copy(session->duck, native_vram, kVramWords) < 0)
gpu_hd_textures_reset_tracking();
}
namespace {
constexpr uint32_t kResidencyMagic = 0x52444850u; /* "PHDR" */
constexpr uint32_t kResidencyVersion = 2u; /* 2: + the pack's identity */
constexpr size_t kResidencyHeader = 16u;
void put32(uint8_t* at, uint32_t v) { for (int i = 0; i < 4; ++i) at[i] = uint8_t(v >> (8 * i)); }
uint32_t get32(const uint8_t* at) { uint32_t v = 0; for (int i = 0; i < 4; ++i) v |= uint32_t(at[i]) << (8 * i); return v; }
}
extern "C" int gpu_hd_textures_residency_save(uint8_t** data, size_t* size) {
if (data) *data = nullptr;
if (size) *size = 0;
if (!data || !size || !session || !session->beetle || !native_vram) return 0;
uint8_t* tracking = nullptr; size_t tracking_size = 0;
if (!hd_texture_pack_tracking_state_save(session->beetle, &tracking, &tracking_size)) return 0;
uint8_t* out = static_cast<uint8_t*>(std::malloc(kResidencyHeader + tracking_size));
if (!out) { std::free(tracking); return 0; }
put32(out, kResidencyMagic); put32(out + 4, kResidencyVersion);
put32(out + 8, hd_texture_crc32_words_le(native_vram, kVramWords));
put32(out + 12, hd_texture_pack_identity(session->beetle));
if (tracking_size) std::memcpy(out + kResidencyHeader, tracking, tracking_size);
std::free(tracking);
*data = out; *size = kResidencyHeader + tracking_size;
return 1;
}
extern "C" int gpu_hd_textures_residency_load(const uint8_t* data, size_t size) {
if (!data || size < kResidencyHeader || !session || !session->beetle || !native_vram) return 0;
if (get32(data) != kResidencyMagic || get32(data + 4) != kResidencyVersion) return 0;
/* Saved with another pack: its residency names other uploads. */
if (get32(data + 12) != hd_texture_pack_identity(session->beetle)) return 0;
if (get32(data + 8) != hd_texture_crc32_words_le(native_vram, kVramWords)) return 0;
if (!hd_texture_pack_tracking_state_check(data + kResidencyHeader, size - kResidencyHeader)) return 0;
return hd_texture_pack_tracking_state_load(session->beetle, data + kResidencyHeader,
size - kResidencyHeader);
}
extern "C" void gpu_hd_textures_invalidate(int x, int y, int width, int height) {
if (!session || width < 1 || height < 1) return;
if (width > 1024 || height > 512) { gpu_hd_textures_reset_tracking(); return; }
hd_texture_pack_invalidate(session->beetle, x & 1023, y & 511, width, height);
duck_texture_pack_invalidate(session->duck, x & 1023, y & 511, width, height);
}
extern "C" void gpu_hd_textures_track_upload(int x, int y, int width, int height,
const uint16_t* words) {
(void)words;
if (!session || !native_vram || width < 1 || width > 1024 || height < 1 || height > 512) return;
if (width == 1024 && height == 512) { gpu_hd_textures_reset_tracking(); return; }
/* gpu.c's A0 staging already contains post-mask words. Read the canonical
* mirror here also for host-initiated transfers which use raw input. */
const size_t count = static_cast<size_t>(width) * height;
std::unique_ptr<uint16_t[]> applied(new (std::nothrow) uint16_t[count]);
if (!applied) { gpu_hd_textures_reset_tracking(); return; }
for (int row = 0; row < height; ++row)
for (int col = 0; col < width; ++col)
applied[static_cast<size_t>(row) * width + col] =
native_vram[((y + row) & 511) * 1024 + ((x + col) & 1023)];
uint32_t hash = 0;
hd_texture_pack_track_upload(session->beetle, x & 1023, y & 511, width, height,
applied.get(), count, &hash);
duck_texture_pack_track_upload(session->duck, x & 1023, y & 511, width, height,
native_vram, kVramWords);
if (session->beetle && hd_texture_pack_has_texture(session->beetle, hash)) {
const std::array<uint16_t, 4> rect{uint16_t(x & 1023), uint16_t(y & 511),
uint16_t(width), uint16_t(height)};
if (std::find(session->learned.begin(), session->learned.end(), rect) == session->learned.end()) {
/* Bounded by count and by area: a restage re-hashes at most one
* VRAM's worth of words (~1 MiB of CRC), however large or many
* the learned uploads (oldest dropped first). */
session->learned.push_back(rect);
size_t area = 0;
for (const auto& r : session->learned) area += size_t(r[2]) * r[3];
while (session->learned.size() > 1 &&
(session->learned.size() > 256 || area > kVramWords)) {
const auto& r = session->learned.front();
area -= size_t(r[2]) * r[3];
session->learned.erase(session->learned.begin());
}
}
}
}
extern "C" void gpu_hd_textures_restage(void) {
gpu_hd_textures_reset_tracking();
if (!session || !session->beetle || !native_vram) return;
/* Only a rectangle whose restored words still hash to a replacement key
* becomes resident again: the same words at the same place as an upload
* this session saw, so no identity is invented. */
std::vector<uint16_t> words;
for (const auto& r : session->learned) {
const size_t count = size_t(r[2]) * r[3];
words.resize(count);
for (unsigned row = 0; row < r[3]; ++row)
for (unsigned col = 0; col < r[2]; ++col)
words[size_t(row) * r[2] + col] = native_vram[((r[1] + row) & 511) * 1024 + ((r[0] + col) & 1023)];
if (hd_texture_pack_has_texture(session->beetle, hd_texture_crc32_words_le(words.data(), count)))
hd_texture_pack_track_upload(session->beetle, r[0], r[1], r[2], r[3], words.data(), count, nullptr);
}
}
static int acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy,
const int limits[4], uint32_t window,
int semi, GpuHdTextureImage* image, bool allow_gl_cache) {
if (!image) return 0;
*image = {};
if (!session) return 0;
++session->diag.draw_queries;
HdTextureDrawQuery query{};
if (!make_query(tp, cx, cy, limits, window, query)) return 0;
dump_query(query, semi);
if (!session->replacements) return 0;
auto lease = std::unique_ptr<Lease>(new (std::nothrow) Lease);
if (!lease) return 0;
if (session->beetle) {
HdTextureMatch match{};
if (hd_texture_pack_match(session->beetle, &query, &match) != HD_TEXTURE_LOOKUP_FOUND) return 0;
++session->diag.matched_draws;
hd_texture_pack_request_decode(session->beetle, match.entry.texture_hash, match.entry.palette_hash);
if (hd_texture_pack_acquire_decoded(session->beetle, match.entry.texture_hash,
match.entry.palette_hash, &lease->beetle) != HD_TEXTURE_LOOKUP_FOUND) return 0;
const unsigned pixels_per_word = 4u >> query.depth;
image->rgba = lease->beetle.rgba; image->width = lease->beetle.width;
image->height = lease->beetle.height; image->stride = lease->beetle.stride;
image->source_width = match.upload_width_words * pixels_per_word;
image->source_height = match.upload_height;
image->origin_u = query.u_first - (match.source_word_x * pixels_per_word + (query.u_first % pixels_per_word));
image->origin_v = query.v_first - match.source_y;
image->cache_key = (static_cast<uint64_t>(match.entry.texture_hash) << 32) | match.entry.palette_hash;
image->alpha_mode = 1;
} else {
try {
std::array<DuckTextureMatch,kMaxParts> matches{}; size_t count = 0;
if (duck_texture_pack_match_parts(session->duck, &query, semi, matches.data(), matches.size(), &count) != HD_TEXTURE_LOOKUP_FOUND || !count) return 0;
++session->diag.matched_draws;
const auto& match = matches[0];
if (count == 1 && match.clip_u <= query.u_first && match.clip_v <= query.v_first &&
match.clip_u + match.clip_width > query.u_last && match.clip_v + match.clip_height > query.v_last) {
/* The GPU copy outlives the decoder's smaller CPU LRU. A draw
* using that copy must not queue another PNG decode. */
if (allow_gl_cache && gl_renderer_hd_texture_cached(match.entry_id, generation,
&image->width, &image->height)) {
image->stride = image->width * 4u;
} else {
duck_texture_pack_request_decode(session->duck, match.entry_id);
if (duck_texture_pack_acquire_decoded(session->duck, match.entry_id, &lease->duck) != HD_TEXTURE_LOOKUP_FOUND) return 0;
image->rgba = lease->duck.rgba; image->width = lease->duck.width;
image->height = lease->duck.height; image->stride = lease->duck.stride;
}
image->source_width = match.source_width; image->source_height = match.source_height;
image->origin_u = match.origin_u; image->origin_v = match.origin_v;
image->cache_key = match.entry_id;
image->alpha_mode = match.key.semitransparent ? 3 : 2;
} else {
std::array<uint16_t,256> palette{};
auto signature = composition_signature(query, matches, count, palette);
const uint64_t full_mask = count == 64 ? UINT64_MAX : (UINT64_C(1) << count) - 1;
/* An immutable, complete composition already owns all source
* pixels. Reuse it even after those decoded PNGs are evicted.
* Partial compositions still check for newly ready parts. */
lease->composition = find_composition(signature, full_mask);
if (!lease->composition) {
DecodedParts decoded; size_t ready = 0;
for (size_t i = 0; i < count; ++i) {
duck_texture_pack_request_decode(session->duck, matches[i].entry_id);
if (duck_texture_pack_acquire_decoded(session->duck, matches[i].entry_id, &decoded.pixels[i]) == HD_TEXTURE_LOOKUP_FOUND) ++ready;
}
if (!ready) return 0;
lease->composition = compose(query, matches, decoded, count, std::move(signature), palette);
}
if (!lease->composition) return 0;
const auto& composed = *lease->composition;
image->rgba = composed.rgba.data(); image->width = composed.width;
image->height = composed.height; image->stride = composed.width * 4u;
image->source_width = query.u_last - query.u_first + 1;
image->source_height = query.v_last - query.v_first + 1;
image->origin_u = query.u_first; image->origin_v = query.v_first;
image->cache_key = composed.cache_key; image->alpha_mode = 4;
}
} catch (const std::exception&) { return 0; }
}
image->generation = generation; image->lease = lease.release();
++session->diag.ready_draws;
return 1;
}
extern "C" int gpu_hd_textures_acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy,
const int limits[4], uint32_t window, int semi, GpuHdTextureImage* image) {
return acquire_draw(tp, cx, cy, limits, window, semi, image, false);
}
extern "C" int gpu_hd_textures_acquire_gl_draw(uint16_t tp, uint16_t cx, uint16_t cy,
const int limits[4], uint32_t window, int semi, GpuHdTextureImage* image) {
return acquire_draw(tp, cx, cy, limits, window, semi, image, true);
}
extern "C" void gpu_hd_textures_release_image(GpuHdTextureImage* image) {
if (!image) return;
delete static_cast<Lease*>(image->lease); *image = {};
}
extern "C" void gpu_hd_textures_observe_draw(uint16_t tp, uint16_t cx, uint16_t cy,
const int limits[4], uint32_t window, int semi) {
if (!session || !session->dump || session->dump_failed) return;
HdTextureDrawQuery query{};
if (make_query(tp, cx, cy, limits, window, query)) {
++session->diag.draw_queries; dump_query(query, semi);
}
}