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;