#include "gpu_hd_textures.h" #include "gpu_gl_renderer.h" #include "hd_texture_pack.h" #include "duckstation_texture_pack.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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 signature; std::vector 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> compositions; size_t composition_bytes = 0; uint64_t composition_clock = 0; std::string dump_error; std::deque 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> learned; GpuHdTextureDiag diag{}; ~Session() { hd_texture_pack_destroy(beetle); duck_texture_pack_destroy(duck); } }; struct Lease { HdTexturePixels beetle{}; DuckTexturePixels duck{}; std::shared_ptr composition; ~Lease() { hd_texture_pixels_release(&beetle); duck_texture_pixels_release(&duck); } }; std::unique_ptr session; const uint16_t* native_vram = nullptr; uint64_t generation = 1; uint64_t composition_serial = 0; struct DecodedParts { std::array 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(first[0]); query.u_last = static_cast(last[0]); query.v_first = static_cast(first[1]); query.v_last = static_cast(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(std::floor(x)), 0, static_cast(image.width) - 1); const int ny = std::clamp(static_cast(std::floor(y)), 0, static_cast(image.height) - 1); const uint8_t* nearest = image.rgba + static_cast(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(std::floor(x)), by = static_cast(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(image.width) - 1); const int py = std::clamp(by + dy, 0, static_cast(image.height) - 1); const uint8_t* pixel = image.rgba + static_cast(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(std::clamp(sum[channel] / weight + 0.5, 0.0, 255.0)) : nearest[channel]; } void native_pixel(const HdTextureDrawQuery& query, const std::array& 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(((word & 31u) * 255u + 15u) / 31u); output[1] = static_cast((((word >> 5) & 31u) * 255u + 15u) / 31u); output[2] = static_cast((((word >> 10) & 31u) * 255u + 15u) / 31u); output[3] = !word ? 0 : (word & 0x8000u) ? 128 : 255; } std::vector composition_signature(const HdTextureDrawQuery& query, const std::array& matches, size_t count, std::array& 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 signature{ static_cast(query.page_x) | (static_cast(query.page_y) << 16) | (static_cast(query.depth) << 32), static_cast(query.u_first) | (static_cast(query.v_first) << 8) | (static_cast(query.u_last) << 16) | (static_cast(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(match.origin_u), static_cast(match.origin_v), match.source_width, match.source_height, static_cast(match.clip_u), static_cast(match.clip_v), match.clip_width, match.clip_height, match.key.semitransparent}); } return signature; } std::shared_ptr find_composition(const std::vector& 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 compose(const HdTextureDrawQuery& query, const std::array& matches, const DecodedParts& decoded, size_t count, std::vector signature, const std::array& 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(pixel.width) / matches[i].source_width); scale_y = std::max(scale_y, static_cast(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(wanted_width), height = static_cast(wanted_height); const size_t bytes = static_cast(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(); ++session->diag.composition_builds; session->diag.composition_pixels += static_cast(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(x) * 2 + 1) * native_width / (width * 2u), query.v_first + (static_cast(y) * 2 + 1) * native_height / (height * 2u), result->rgba.data() + (static_cast(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(std::floor((match.clip_u - query.u_first) * static_cast(width) / native_width)), 0, static_cast(width)); const int y0 = std::clamp(static_cast(std::floor((match.clip_v - query.v_first) * static_cast(height) / native_height)), 0, static_cast(height)); const int x1 = std::clamp(static_cast(std::ceil((match.clip_u + match.clip_width - query.u_first) * static_cast(width) / native_width)), 0, static_cast(width)); const int y1 = std::clamp(static_cast(std::ceil((match.clip_v + match.clip_height - query.v_first) * static_cast(height) / native_height)), 0, static_cast(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(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(); 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(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(width) * height; std::unique_ptr 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(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 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 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(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(match.entry.texture_hash) << 32) | match.entry.palette_hash; image->alpha_mode = 1; } else { try { std::array 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 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(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); } }