mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 19:07:47 -04:00
tex_pack: position-exact dedup; savestate-section groundwork (dormant)
DEDUP: a re-upload of identical content at a DIFFERENT rect was deduped by hash+size, keeping the stale rect -- origins computed from it were off by the position drift. Dedup now requires the same position; the origin/src handed to the renderer come from the live containing upload. NOTE: this did NOT cure the strip vertical offset (top row cut, bottom duplicated) -- that symptom persists on screen and its root cause is still unidentified. The change stands on its own correctness, not as that fix. SAVESTATE SECTION (BS_SEC_TEXPACK): written and applied correctly in isolation (telemetry showed 1360 entries rebuilt from the state's own VRAM), but with the section present the overall state load FAILS in a LATER section, cause not yet isolated -- and old builds hard-reject unknown tags (`default: return 0`), so the section also breaks backward compatibility. The boot_state wiring is REVERTED; tex_pack_state_bytes/write/apply remain, dormant, for whoever picks this up with the loader diagnosis. Until then: after loading a savestate, textures the game does not re-upload stay low-res -- known limitation again. The first-save defer fix verified working in passing: "deferring slot 4 -- no safe resume PC (hint=0x00000000)" followed by a save at a real PC. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,7 @@ enum {
|
||||
apart (MotK abort@940: fin cyc Δ8, v0 5c83/5c86
|
||||
from identical baselines). Optional on load for
|
||||
old blobs (left untouched when absent). */
|
||||
BS_SEC_TEXPACK = 0x11, /* HD texture pack upload tracker (rects+hashes) */
|
||||
};
|
||||
|
||||
/* Save a COMPLETE snapshot at game handoff. Returns 1 on success. */
|
||||
|
||||
@@ -55,6 +55,13 @@ void tex_pack_init(const char *disc_path, int enable_replace, int enable_dump,
|
||||
const char *dir_override, const char *pack_dir);
|
||||
void tex_pack_shutdown(void);
|
||||
|
||||
/* Savestate section: rects+hashes of tracked uploads (pixels rebuilt from the
|
||||
* restored VRAM and hash-verified at apply). Apply AFTER VRAM is restored. */
|
||||
uint32_t tex_pack_state_bytes(void);
|
||||
void tex_pack_state_write(uint8_t *p);
|
||||
void tex_pack_state_apply(const uint8_t *p, uint64_t len,
|
||||
const uint16_t *vram /* NULL = CPU mirror */);
|
||||
|
||||
/* Write <pack dir>/coverage.json: how much of the active pack the session
|
||||
* actually drew, plus the entries it never asked for. Safe to call from
|
||||
* std::atexit and safe to call more than once; a no-op when no pack is active.
|
||||
|
||||
+138
-13
@@ -179,6 +179,8 @@ struct State {
|
||||
};
|
||||
std::vector<NoContain> nocontain;
|
||||
uint64_t n_uploads = 0, n_upload_dedup = 0, n_kills = 0;
|
||||
uint64_t n_restore_kept = 0; /* uploads surviving savestate-restore revalidation */
|
||||
uint64_t n_state_calls = 0, n_state_dropped = 0; /* TEXPACK section apply telemetry */
|
||||
uint64_t n_prims = 0, n_pal_hash = 0, n_pal_memo_hit = 0;
|
||||
uint64_t n_dump_written = 0, n_dump_failed = 0;
|
||||
};
|
||||
@@ -527,6 +529,82 @@ void invalidate_locked(int x, int y, int w, int h) {
|
||||
|
||||
} // namespace
|
||||
|
||||
/* Savestate integration. The upload tracker is what makes HD substitution
|
||||
* possible -- a draw substitutes only when it lands inside a tracked upload --
|
||||
* and a savestate restore repopulates VRAM without uploads. Anything the game
|
||||
* uploads once (boot-time text strips, font atlases) therefore lost its HD
|
||||
* replacement after every load, until the game happened to re-upload it.
|
||||
*
|
||||
* The snapshot stores rects + hashes only (20 bytes/entry): pixels are
|
||||
* reconstructed from the RESTORED VRAM at apply time and verified against the
|
||||
* stored hash, so a kept entry is byte-exact by construction and the section
|
||||
* stays a few KB. Old states without the section simply skip this. */
|
||||
extern "C" uint32_t tex_pack_state_bytes(void) {
|
||||
std::lock_guard<std::mutex> lk(g.mu);
|
||||
return (uint32_t)(4u + g.uploads.size() * 20u);
|
||||
}
|
||||
|
||||
extern "C" void tex_pack_state_write(uint8_t *p) {
|
||||
std::lock_guard<std::mutex> lk(g.mu);
|
||||
const uint32_t n = (uint32_t)g.uploads.size();
|
||||
uint32_t off = 0;
|
||||
auto put32 = [&](uint32_t v) {
|
||||
p[off++] = (uint8_t)(v); p[off++] = (uint8_t)(v >> 8);
|
||||
p[off++] = (uint8_t)(v >> 16); p[off++] = (uint8_t)(v >> 24);
|
||||
};
|
||||
put32(n);
|
||||
for (const Upload &u : g.uploads) {
|
||||
put32((uint32_t)u.x); put32((uint32_t)u.y);
|
||||
put32((uint32_t)u.w); put32((uint32_t)u.h);
|
||||
put32(u.hash);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void tex_pack_state_apply(const uint8_t *p, uint64_t len,
|
||||
const uint16_t *vram) {
|
||||
if (!tex_pack_active() || !p || len < 4) return;
|
||||
/* The caller passes the savestate's OWN VRAM section. gpu_get_vram() is
|
||||
* the CPU mirror, which under the GL-authoritative pipeline is only kept
|
||||
* coherent where something forces it (CLUT rows) -- hashing tracked rects
|
||||
* against a stale mirror would silently drop every entry. */
|
||||
if (!vram) vram = gpu_get_vram();
|
||||
if (!vram) return;
|
||||
std::lock_guard<std::mutex> lk(g.mu);
|
||||
uint32_t off = 0;
|
||||
auto get32 = [&]() {
|
||||
uint32_t v = (uint32_t)p[off] | ((uint32_t)p[off+1] << 8) |
|
||||
((uint32_t)p[off+2] << 16) | ((uint32_t)p[off+3] << 24);
|
||||
off += 4; return v;
|
||||
};
|
||||
g.n_state_calls++;
|
||||
const uint32_t n = get32();
|
||||
if (len < 4ull + (uint64_t)n * 20ull) return;
|
||||
g.uploads.clear();
|
||||
g.pal_memo.clear();
|
||||
size_t kept = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
const int x = (int)get32(), y = (int)get32();
|
||||
const int w = (int)get32(), h = (int)get32();
|
||||
const uint32_t hash = get32();
|
||||
if (w <= 0 || h <= 0 || x < 0 || y < 0 ||
|
||||
x + w > FB_WIDTH || y + h > FB_HEIGHT) continue;
|
||||
Upload up;
|
||||
up.x = x; up.y = y; up.w = w; up.h = h; up.hash = hash;
|
||||
up.pixels.resize((size_t)w * (size_t)h);
|
||||
for (int row = 0; row < h; row++)
|
||||
std::memcpy(up.pixels.data() + (size_t)row * w,
|
||||
vram + ((size_t)(y + row) * FB_WIDTH + x),
|
||||
(size_t)w * sizeof(uint16_t));
|
||||
if (crc32_compute((const uint8_t *)up.pixels.data(),
|
||||
up.pixels.size() * sizeof(uint16_t)) != hash)
|
||||
continue; /* VRAM diverged from this rect since it was saved */
|
||||
g.uploads.push_back(std::move(up));
|
||||
if (++kept >= MAX_UPLOADS) break;
|
||||
}
|
||||
g.n_restore_kept += kept;
|
||||
g.n_state_dropped += (uint64_t)n - kept;
|
||||
}
|
||||
|
||||
extern "C" void tex_pack_invalidate(int x, int y, int w, int h) {
|
||||
if (!tex_pack_active()) return;
|
||||
std::lock_guard<std::mutex> lk(g.mu);
|
||||
@@ -539,10 +617,40 @@ extern "C" void tex_pack_on_upload(int x, int y, int w, int h, const uint16_t *p
|
||||
|
||||
std::lock_guard<std::mutex> lk(g.mu);
|
||||
|
||||
/* A full-VRAM transfer is a savestate restore, not a texture; Beetle drops
|
||||
* these too, and tracking one would alias every subsequent lookup. */
|
||||
/* A full-VRAM transfer is a savestate restore, not a texture. Beetle drops
|
||||
* these from tracking too -- but DROPPING EVERY TRACKED UPLOAD is what
|
||||
* made HD replacements (including the fonts, which are pack entries) fall
|
||||
* back to low-res after loading a state: substitution requires a draw to
|
||||
* land inside a tracked upload, the restore repopulates VRAM without any
|
||||
* uploads, and textures the game only uploads once (boot-time text strips)
|
||||
* never track again for the rest of the session.
|
||||
*
|
||||
* We can do better than dropping, exactly: each Upload carries its pixel
|
||||
* preimage, and this transfer carries the complete incoming VRAM. Keep an
|
||||
* entry iff its rect in the incoming image is byte-identical to the
|
||||
* preimage -- then its hash is still true of what VRAM now holds, which is
|
||||
* the precise condition substitution relies on. Anything the state differs
|
||||
* on is dropped as before. Palette memos stay dropped: they are keyed on
|
||||
* position and re-hash lazily from live VRAM, so correctness is unaffected.
|
||||
*/
|
||||
if (w == FB_WIDTH && h == FB_HEIGHT) {
|
||||
invalidate_locked(0, 0, FB_WIDTH, FB_HEIGHT);
|
||||
size_t kept = 0;
|
||||
for (size_t i = 0; i < g.uploads.size(); ) {
|
||||
const Upload &u = g.uploads[i];
|
||||
bool same = (u.x >= 0 && u.y >= 0 &&
|
||||
u.x + u.w <= FB_WIDTH && u.y + u.h <= FB_HEIGHT &&
|
||||
u.pixels.size() == (size_t)u.w * (size_t)u.h);
|
||||
for (int row = 0; same && row < u.h; row++) {
|
||||
const uint16_t *inc = pixels + ((size_t)(u.y + row) * FB_WIDTH + u.x);
|
||||
if (std::memcmp(inc, u.pixels.data() + (size_t)row * u.w,
|
||||
(size_t)u.w * sizeof(uint16_t)) != 0)
|
||||
same = false;
|
||||
}
|
||||
if (same) { kept++; i++; }
|
||||
else { g.n_kills++; g.uploads.erase(g.uploads.begin() + (ptrdiff_t)i); }
|
||||
}
|
||||
g.n_restore_kept += kept;
|
||||
g.pal_memo.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -554,7 +662,15 @@ extern "C" void tex_pack_on_upload(int x, int y, int w, int h, const uint16_t *p
|
||||
const uint32_t hash = crc32_compute((const uint8_t *)pixels, n * sizeof(uint16_t));
|
||||
|
||||
for (const Upload &u : g.uploads) {
|
||||
if (u.hash == hash && u.w == w && u.h == h) { g.n_upload_dedup++; return; }
|
||||
/* Dedup only when the POSITION matches too. The game re-uploads some
|
||||
* textures (the UI text strips) at a rect one scanline off from the
|
||||
* first upload; deduping by content alone kept the STALE rect, so the
|
||||
* draw's origin was computed one texel high -- glyph tops cut off,
|
||||
* bottoms duplicated, and under LINEAR sampling that same one-texel
|
||||
* error is the neighbour-bleed seam. Same content at a new position is
|
||||
* a new upload; invalidate_locked above already retired any overlap. */
|
||||
if (u.hash == hash && u.w == w && u.h == h &&
|
||||
u.x == x && u.y == y) { g.n_upload_dedup++; return; }
|
||||
}
|
||||
|
||||
Upload up;
|
||||
@@ -776,15 +892,20 @@ extern "C" int tex_pack_lookup_replacement(const int lim[4], uint16_t clut_x,
|
||||
out->pixels = r->pixels.data();
|
||||
out->width = r->w;
|
||||
out->height = r->h;
|
||||
/* Cached from this entry's first decode. Recomputing them from the
|
||||
* live draw is more correct in principle -- origin is relative to the
|
||||
* CURRENT texture page -- but doing so visibly reintroduced the glyph
|
||||
* seams the texel-centre fix removed, so the two are coupled and the
|
||||
* live version needs the sampling reworked with it, not on its own. */
|
||||
out->src_w = r->src_w;
|
||||
out->src_h = r->src_h;
|
||||
out->origin_u = r->origin_u;
|
||||
out->origin_v = r->origin_v;
|
||||
/* From the LIVE containing upload, not the entry's first decode. The
|
||||
* Repl caches the origin of whichever upload triggered its decode, but
|
||||
* origin is a property of THIS draw's upload and texture page. With
|
||||
* position-exact dedup above, `up` is the rect the draw actually
|
||||
* samples, so this is now correct where the first attempt (which read
|
||||
* the stale deduped rect) was not.
|
||||
*
|
||||
* src stays derived from the upload too: same texture content always
|
||||
* has the same dimensions, so this only ever differs from the cache by
|
||||
* the drifted position. */
|
||||
out->src_w = up.w << shift;
|
||||
out->src_h = up.h;
|
||||
out->origin_u = (up.x - base_x) << shift;
|
||||
out->origin_v = up.y - base_y;
|
||||
out->id = r->key;
|
||||
out->gl_handle = &r->gl_handle;
|
||||
out->recolour = recolour;
|
||||
@@ -956,6 +1077,7 @@ extern "C" int tex_pack_debug_json(const char *subcmd, char *out, int cap) {
|
||||
"\"repl_hit\":%llu,\"repl_miss\":%llu,"
|
||||
"\"repl_recolour\":%llu,\"repl_multicol\":%llu,"
|
||||
"\"repl_calls\":%llu,\"repl_nocontain\":%llu,"
|
||||
"\"restore_kept\":%llu,\"state_calls\":%llu,\"state_dropped\":%llu,"
|
||||
"\"repl_notex\":%llu}",
|
||||
(int)g.replace_on, (int)g.dump_on,
|
||||
pack_dir.c_str(), dump_dir.c_str(),
|
||||
@@ -972,6 +1094,9 @@ extern "C" int tex_pack_debug_json(const char *subcmd, char *out, int cap) {
|
||||
(unsigned long long)g.n_repl_multicol,
|
||||
(unsigned long long)g.n_repl_calls,
|
||||
(unsigned long long)g.n_repl_nocontain,
|
||||
(unsigned long long)g.n_restore_kept,
|
||||
(unsigned long long)g.n_state_calls,
|
||||
(unsigned long long)g.n_state_dropped,
|
||||
(unsigned long long)g.n_repl_notex);
|
||||
} else if (!std::strcmp(subcmd, "uploads")) {
|
||||
n = std::snprintf(out, (size_t)cap, "[");
|
||||
|
||||
Reference in New Issue
Block a user