From f7fed074f1df5d700a316ee259015c8d45287fcc Mon Sep 17 00:00:00 2001 From: yacineolivier-maker <234833416+yacineolivier-maker@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:59:04 +0200 Subject: [PATCH] HD textures: filtered sampling, a cache sized for a whole pack, indexed keys - 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 Claude-Session: https://claude.ai/code/session_01Rkyma8rbyPiccs9n2bkyG6 --- runtime/include/hd_texture_pack.h | 9 ++ runtime/src/debug_server.c | 11 +++ runtime/src/duckstation_texture_pack.cpp | 23 +++++ runtime/src/gpu_gl_renderer.c | 118 +++++++++++++++++++---- runtime/src/gpu_hd_textures.cpp | 6 +- runtime/src/hd_texture_pack.cpp | 37 ++++++- 6 files changed, 181 insertions(+), 23 deletions(-) diff --git a/runtime/include/hd_texture_pack.h b/runtime/include/hd_texture_pack.h index af389ffe..d15da23f 100644 --- a/runtime/include/hd_texture_pack.h +++ b/runtime/include/hd_texture_pack.h @@ -25,6 +25,13 @@ enum HdTextureLookupStatus { HD_TEXTURE_LOOKUP_AMBIGUOUS = 2 }; +/* Palette identity of an indexed replacement, named TTTTTTTT-index.png: its + * red channel holds CLUT indices (alpha 0 = transparent) and the renderer + * resolves them through the live CLUT of each draw, so one image serves every + * palette the game draws the texture with (highlights, fades). An exact + * TTTTTTTT-PPPPPPPP image still wins for its own palette. */ +#define HD_TEXTURE_PALETTE_INDEXED 0xFFFFFFFFu + typedef struct HdTexturePackInfo { const char* asset_root; /* valid until hd_texture_pack_destroy */ const char* replacement_root; /* valid until hd_texture_pack_destroy */ @@ -109,6 +116,8 @@ uint32_t hd_texture_hash_clut(const uint16_t* vram, /* Upload bytes are row-major logical upload words, independent of destination * wrapping. Tracking invalidates/splits every intersected older residency. * Width <= 1024 and height <= 512; X/Y may wrap. */ +int hd_texture_pack_debug_uploads_at(const HdTexturePack* pack, unsigned x, unsigned y, + char* out, size_t cap); int hd_texture_pack_track_upload(HdTexturePack* pack, uint16_t x, uint16_t y, diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index e61094f5..abca84bd 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -9469,6 +9469,16 @@ static void handle_perf(int id, const char *json) { send_fmt("{\"id\":%d,\"ok\":true,%s}", id, buf); } +/* hd_uploads x= y= -- texture uploads covering that VRAM + * point (newest first): hash, size, fragment, whether the pack replaces it. */ +extern int gpu_hd_textures_debug_uploads_at(int x, int y, char *out, size_t cap); +static void handle_hd_uploads(int id, const char *json) { + static char buf[16384]; + int n = gpu_hd_textures_debug_uploads_at(json_get_int(json, "x", 0), json_get_int(json, "y", 0), + buf, sizeof buf); + send_fmt("{\"id\":%d,\"ok\":true,\"count\":%d,\"uploads\":[%s]}", id, n, buf); +} + static void handle_display_aspect(int id, const char *json) { int num=json_get_int(json,"num",-1), den=json_get_int(json,"den",-1); int adaptive=json_get_int(json,"adaptive",0); @@ -16393,6 +16403,7 @@ static const CmdEntry s_commands[] = { { "ws_aspect", handle_ws_aspect }, { "display_aspect", handle_display_aspect }, { "perf", handle_perf }, + { "hd_uploads", handle_hd_uploads }, { "window_size", handle_window_size }, { "ws_nw", handle_ws_nw }, { "debug_key", handle_debug_key }, diff --git a/runtime/src/duckstation_texture_pack.cpp b/runtime/src/duckstation_texture_pack.cpp index e577638f..ce0fb119 100644 --- a/runtime/src/duckstation_texture_pack.cpp +++ b/runtime/src/duckstation_texture_pack.cpp @@ -827,6 +827,29 @@ void duck_texture_pack_invalidate(DuckTexturePack* p, uint16_t x, uint16_t y, ui const unsigned w0 = std::min(unsigned(w), 1024 - px), h0 = std::min(unsigned(h), 512 - py); const std::array regions{{{px,py,w0,h0},{0,py,unsigned(w)-w0,h0}, {px,0,w0,unsigned(h)-h0},{0,0,unsigned(w)-w0,unsigned(h)-h0}}}; + /* Under HD authority every native draw lands here, almost always on the + * frame buffer, which no tracked texture overlaps: leave without touching + * (or allocating) anything when no residency or cache entry intersects. */ + const auto touches = [&](const Rect& x) { + for (const auto& r : regions) if (r.w && r.h && intersects(x,r)) return true; + return false; + }; + bool hit = false; + for (const auto& upload : p->uploads) if (touches(upload.active)) { hit = true; break; } + if (!hit) for (const auto& page : p->pages) { + if (touches(page.source->rect)) { hit = true; break; } + for (const auto& record : page.source->records) + if (record.depth < 2 && touches({record.clut_x,record.clut_y,record.palette_size,1})) { hit = true; break; } + if (hit) break; + } + if (!hit) for (const auto& v : p->rect_cache) if (touches(v.rect)) { hit = true; break; } + if (!hit) for (const auto& c : p->match_cache) { + const auto& q = c.query; + const Rect page{q.page_x,q.page_y,256 / expansion(q.depth),256}; + const Rect clut{q.clut_x,q.clut_y,std::min(q.depth == 0 ? 16u : 256u,1024u-q.clut_x),1}; + if (touches(page) || (q.depth < 2 && touches(clut))) { hit = true; break; } + } + if (!hit) return; ++p->revision; bool released = false; /* a residency record dropped or split */ for (const auto& r : regions) if (r.w && r.h) { diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 15a853a2..2b6fc641 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -198,6 +198,7 @@ typedef void (APIENTRY *PFN_glBlendEquationSeparate)(GLenum, GLenum); typedef void (APIENTRY *PFN_glGenVertexArrays)(GLsizei, GLuint *); typedef void (APIENTRY *PFN_glBindVertexArray)(GLuint); typedef void (APIENTRY *PFN_glActiveTexture)(GLenum); +typedef void (APIENTRY *PFN_glGenerateMipmap)(GLenum); typedef void (APIENTRY *PFN_glGenBuffers)(GLsizei, GLuint *); typedef void (APIENTRY *PFN_glDeleteBuffers)(GLsizei, const GLuint *); typedef void (APIENTRY *PFN_glDeleteVertexArrays)(GLsizei, const GLuint *); @@ -271,6 +272,7 @@ static PFN_glBlendEquationSeparate p_glBlendEquationSeparate; static PFN_glGenVertexArrays p_glGenVertexArrays; static PFN_glBindVertexArray p_glBindVertexArray; static PFN_glActiveTexture p_glActiveTexture; +static PFN_glGenerateMipmap p_glGenerateMipmap; static PFN_glGenBuffers p_glGenBuffers; static PFN_glDeleteBuffers p_glDeleteBuffers; static PFN_glDeleteVertexArrays p_glDeleteVertexArrays; @@ -352,6 +354,7 @@ static int load_modern_gl(void) { LOAD(p_glBlendEquationSeparate, "glBlendEquationSeparate"); LOAD(p_glGenVertexArrays, "glGenVertexArrays"); LOAD(p_glBindVertexArray, "glBindVertexArray"); LOAD(p_glActiveTexture, "glActiveTexture"); LOAD(p_glGenBuffers, "glGenBuffers"); + LOAD(p_glGenerateMipmap, "glGenerateMipmap"); LOAD(p_glDeleteBuffers, "glDeleteBuffers"); LOAD(p_glDeleteVertexArrays, "glDeleteVertexArrays"); LOAD(p_glBindBuffer, "glBindBuffer"); LOAD(p_glBufferData, "glBufferData"); LOAD(p_glVertexAttribPointer, "glVertexAttribPointer"); @@ -2011,12 +2014,32 @@ static const char *TEX_FS = " p &= ivec2(255);\n" " ivec4 tw=ivec4(v_twin&31,(v_twin>>5)&31,(v_twin>>10)&31,(v_twin>>15)&31);\n" " if((tw.x|tw.y)!=0) p=(p & ~(tw.xy*8)) | ((tw.zw & tw.xy)*8);\n" - " else p=clamp(p,v_limits.xy,v_limits.zw);\n" + " else {\n" + /* Clamped to the primitive's UV bounds: the sub-texel position goes to + * the clamped edge too. Keeping the fraction of an outside sample + * (u=39.99 -> texel 40 at 0.99) reads the replacement deep inside the + * edge texel, a 1px sliver of the next glyph at the sprite border. */ + " ivec2 q=clamp(p,v_limits.xy,v_limits.zw);\n" + " f=vec2(q.x>p.x?0.0:(q.xp.y?0.0:(q.y>15)&1; ++hit; }\n" + " }\n" + " if(hit*2 16384) mib = 16384; + budget = (size_t)mib * 1024u * 1024u; + } + return budget; +} +#define HD_GL_CACHE_BUDGET hd_gl_cache_budget() typedef struct HdGlTexture { uint64_t generation, key; uint64_t last_use; @@ -3509,6 +3566,24 @@ typedef struct HdGlTexture { static HdGlTexture s_hd_gl_cache[HD_GL_CACHE_CAP]; static size_t s_hd_gl_cache_bytes; static uint64_t s_hd_gl_cache_clock; +static int16_t s_hd_gl_hint[HD_GL_HINTS]; /* slot + 1, 0 = none */ +static unsigned hd_gl_hint_of(uint64_t key, uint64_t generation) { + uint64_t h = (key ^ (generation * 0x9E3779B97F4A7C15ull)) * 0xD6E8FEB86659FD93ull; + return (unsigned)(h >> 51) & (HD_GL_HINTS - 1); +} +static int hd_gl_find(uint64_t key, uint64_t generation) { + const unsigned h = hd_gl_hint_of(key, generation); + const int hinted = s_hd_gl_hint[h] - 1; + if (hinted >= 0 && s_hd_gl_cache[hinted].texture && s_hd_gl_cache[hinted].key == key && + s_hd_gl_cache[hinted].generation == generation) return hinted; + for (int i = 0; i < HD_GL_CACHE_CAP; ++i) { + const HdGlTexture* texture = &s_hd_gl_cache[i]; + if (texture->texture && texture->key == key && texture->generation == generation) { + s_hd_gl_hint[h] = (int16_t)(i + 1); return i; + } + } + return -1; +} void gl_renderer_clear_hd_texture_cache(void) { /* Pack (re)configuration: the render thread must be idle before the @@ -3520,28 +3595,23 @@ void gl_renderer_clear_hd_texture_cache(void) { glDeleteTextures(1, &s_hd_gl_cache[i].texture); } memset(s_hd_gl_cache, 0, sizeof(s_hd_gl_cache)); + memset(s_hd_gl_hint, 0, sizeof(s_hd_gl_hint)); s_hd_gl_cache_bytes = 0; s_tb_hd_tex = 0; } int gl_renderer_hd_texture_cached(uint64_t key, uint64_t generation, uint32_t* width, uint32_t* height) { - for (int i = 0; i < HD_GL_CACHE_CAP; ++i) { - const HdGlTexture* texture = &s_hd_gl_cache[i]; - if (texture->texture && texture->key == key && texture->generation == generation) { - *width = texture->width; *height = texture->height; return 1; - } - } - return 0; + const int i = hd_gl_find(key, generation); + if (i < 0) return 0; + *width = s_hd_gl_cache[i].width; *height = s_hd_gl_cache[i].height; return 1; } static GLuint hd_gl_texture(const GpuHdTextureImage* image) { int free_slot = -1; - for (int i = 0; i < HD_GL_CACHE_CAP; ++i) { - HdGlTexture* texture = &s_hd_gl_cache[i]; - if (texture->texture && texture->key == image->cache_key && - texture->generation == image->generation) { - texture->last_use = ++s_hd_gl_cache_clock; return texture->texture; - } - if (!texture->texture && free_slot < 0) free_slot = i; + const int found = hd_gl_find(image->cache_key, image->generation); + if (found >= 0) { + s_hd_gl_cache[found].last_use = ++s_hd_gl_cache_clock; return s_hd_gl_cache[found].texture; } + for (int i = 0; i < HD_GL_CACHE_CAP && free_slot < 0; ++i) + if (!s_hd_gl_cache[i].texture) free_slot = i; if (!image->rgba || !image->width || !image->height || !image->source_width || !image->source_height || image->stride != (uint64_t)image->width * 4u || image->width > (uint32_t)s_gl_max_dim || image->height > (uint32_t)s_gl_max_dim) @@ -3567,19 +3637,25 @@ static GLuint hd_gl_texture(const GpuHdTextureImage* image) { glGenTextures(1, &texture->texture); p_glActiveTexture(PSXGL_TEXTURE0 + 2); glBindTexture(GL_TEXTURE_2D, texture->texture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + /* Mip chain + linear filters: the shader samples them only when a texture + * filter is selected (hd_texel_filtered); nearest keeps texelFetch. */ + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + if (s_tex_aniso > 1) + glTexParameterf(GL_TEXTURE_2D, 0x84FE /* GL_TEXTURE_MAX_ANISOTROPY */, (float)s_tex_aniso); glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image->width, image->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->rgba); + if (p_glGenerateMipmap) p_glGenerateMipmap(GL_TEXTURE_2D); p_glActiveTexture(PSXGL_TEXTURE0); if (glGetError() != GL_NO_ERROR) { glDeleteTextures(1, &texture->texture); texture->texture = 0; return 0; } texture->key = image->cache_key; texture->generation = image->generation; texture->last_use = ++s_hd_gl_cache_clock; + s_hd_gl_hint[hd_gl_hint_of(texture->key, texture->generation)] = (int16_t)(free_slot + 1); texture->bytes = bytes; s_hd_gl_cache_bytes += bytes; texture->width = image->width; texture->height = image->height; gpu_hd_textures_note_gl_cache(0); diff --git a/runtime/src/gpu_hd_textures.cpp b/runtime/src/gpu_hd_textures.cpp index 62811dd9..85db9a97 100644 --- a/runtime/src/gpu_hd_textures.cpp +++ b/runtime/src/gpu_hd_textures.cpp @@ -396,6 +396,10 @@ extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) { session->diagnostics.push_back(std::move(diagnostic)); out->diagnostic = session->diagnostics.back().c_str(); } +extern "C" int gpu_hd_textures_debug_uploads_at(int x, int y, char* out, size_t cap) { + if (out && cap) out[0] = ''; + return session ? hd_texture_pack_debug_uploads_at(session->beetle, (unsigned)x, (unsigned)y, out, cap) : 0; +} 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) { @@ -557,7 +561,7 @@ static int acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy, 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; + image->alpha_mode = match.entry.palette_hash == HD_TEXTURE_PALETTE_INDEXED ? 5 : 1; } else { try { std::array matches{}; size_t count = 0; diff --git a/runtime/src/hd_texture_pack.cpp b/runtime/src/hd_texture_pack.cpp index 720b6794..ce913671 100644 --- a/runtime/src/hd_texture_pack.cpp +++ b/runtime/src/hd_texture_pack.cpp @@ -113,6 +113,10 @@ bool parse_key_text(const std::string& text, 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); } @@ -1070,6 +1074,35 @@ int hd_texture_pack_track_upload(HdTexturePack* pack, 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] = ''; + if (!pack) return 0; + for (auto it = pack->uploads.rbegin(); it != pack->uploads.rend(); ++it) { + for (const Fragment& f : it->fragments) { + if (x < f.rect.x || x >= f.rect.x + f.rect.width || + y < f.rect.y || y >= f.rect.y + f.rect.height) continue; + const int w = std::snprintf(out + pos, cap - pos, + "%s{\"serial\":%llu,\"hash\":\"%08x\",\"w\":%u,\"h\":%u," + "\"rect\":[%u,%u,%u,%u],\"src\":[%u,%u],\"in_pack\":%d}", + n ? "," : "", (unsigned long long)it->serial, it->hash, + it->width, it->height, f.rect.x, f.rect.y, f.rect.width, f.rect.height, + f.source_x, f.source_y, pack->texture_hashes.count(it->hash) ? 1 : 0); + if (w < 0 || (size_t)w >= cap - pos) return n; + pos += (size_t)w; + ++n; + break; + } + } + return n; +} + void hd_texture_pack_reset_tracking(HdTexturePack* pack) { if (pack) { pack->uploads.clear(); @@ -1212,7 +1245,9 @@ int hd_texture_pack_match(HdTexturePack* pack, if (indexed == pack->upload_by_serial.end() || !indexed->second) continue; const Upload& upload = *indexed->second; - const auto record = pack->entries.find(make_key(upload.hash, palette_hash)); + auto record = pack->entries.find(make_key(upload.hash, palette_hash)); + if (record == pack->entries.end() && query->depth != HD_TEXTURE_DEPTH_16BPP) + record = pack->entries.find(make_key(upload.hash, HD_TEXTURE_PALETTE_INDEXED)); if (record == pack->entries.end()) continue; if (!covered_by_upload(upload, wanted)) continue; if (record->second.ambiguous) return HD_TEXTURE_LOOKUP_AMBIGUOUS;