mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
2641f7c59b
Framework fixes surfaced while bringing up a title that stresses overlays, HD texture replacement and PGXP at a doubled CRTC rate. All are general; the title's own configuration stays in its game repository. CORRECTNESS * Overlay DLL shim was deleted by the preprocessor. compile_overlays.py injected overlay_dispatch_preamble.c.inc after the syntactically LAST #include. A conditional i-cache include moved that anchor inside an #else, and DLL builds define PSX_OVERLAY_DLL_BUILD, so the callback definitions preprocessed away and every overlay DLL failed to link. Any title using overlays would silently fall back to per-instruction interpretation. The shim is now anchored to the unconditional #include "psx_runtime.h" and raises if that anchor is absent. * Overlay capture kept stale PC bitmaps when executed code later became data, so one variant's PC set could be compiled against another variant's bytes. overlay_watch_note_write now retires the whole page's execution/dispatch epoch when any executed word is overwritten; static demand is keyed by (load_addr, size, bytes) and resolved only against matching range identities. * VRAM-to-VRAM copies only invalidated the destination and never registered it as a replacement candidate, so textures arriving by copy could not be dumped or replaced at all. gr_copy_rect now calls tex_pack_on_copy with the same (texture,palette) key semantics as CPU uploads. * PGXP shared-edge seams: a per-primitive all-or-nothing gate cannot stop two adjacent primitives disagreeing about a shared endpoint, so the edge is submitted at two sub-pixel positions. Adds a frame-stamped canonical endpoint cache keyed by final integer screen position: the first use fixes the coordinates and every later occurrence reuses them. * PGXP provenance: PGXPValue carries per-component X/Y/Z projection ids, so a value assembled from different projections can no longer arm correction. Destructive AND/XOR/NOR/SLT, link and COP0 writes now invalidate shadows, and the SXYP shadow FIFO shifts with guest SXY. * Precise NCLIP no longer replaces guest MAC0. It is telemetry only, so face visibility stays bit-exact with the integer GTE. * Removed two counters that were reported as correctness evidence but were never incremented anywhere. PERFORMANCE * PSX_ENABLE_LTO plus a noinline guard on the shared cycle wrappers. Unguarded ThinLTO clones hot interlock bodies and REGRESSES the hot path; guarded, it becomes a win. Adds runtime/tests/bench_psx_runtime_hotpath.c, which verifies variant equivalence by digest so codegen changes are measurable. * Cycle accounting: emitters select specialized arity-0/1/2/3 helpers at generation time instead of a generic dependency-mask loop; main-RAM loads combine base/wait/completion into one exact charge. Exactness is covered by pipeline-state, load-state, deadline-crossing and cross-device causality tests. * HD texture invalidation replaced a linear scan of up to 8192 upload records per VRAM write with a tile-refcount index; out-of-domain writes early-out. * Translation/text-substitution dispatch is gated behind a monotonic mutation epoch instead of running a callback on every dispatch. * card_data_writes_check no longer runs on every RAM write. CAPABILITY * fixed_outer_aspect: the outer presentation aspect becomes a function of user configuration only. Scene classification selects content transforms, never the outer viewport, so menus/FMV stay centred undistorted inside a wide canvas. VERIFICATION AND LIMITS docs/UPSTREAM_2026-08-20.md audits each change against the diff, marks unsupported claims UNKNOWN, and lists a suggested PR split and review focus. Unit and structural tests accompany each item, but this body of work has NOT been validated end-to-end against a fixed reproducible workload -- reviewers should treat the timing-exactness and PGXP changes as the highest-risk areas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
188 lines
9.3 KiB
C++
188 lines
9.3 KiB
C++
/* tex_pack.h — HD texture replacement / dumping (framework).
|
|
*
|
|
* A reusable psxrecomp feature: identify each texture the game uploads to VRAM
|
|
* by content hash, so a high-resolution replacement image can be substituted at
|
|
* draw time. The on-disk format is deliberately **identical to Beetle PSX HW's**
|
|
* so existing packs authored for RetroArch are drop-in:
|
|
*
|
|
* <disc dir>/<disc stem>-texture-replacements/<texhash>-<palhash>.png
|
|
* <disc dir>/<disc stem>-texture-dump/<texhash>-<palhash>.png
|
|
*
|
|
* where `texhash` is the CRC32 of the raw 16-bit VRAM words of the rectangle the
|
|
* game uploaded via GP0(A0), and `palhash` is the CRC32 of the CLUT row the draw
|
|
* samples it through. Both are lowercase `%x` (no zero padding). Compatibility
|
|
* rests on three things matching Beetle exactly, all verified:
|
|
* - the polynomial (0xEDB88320, init/xorout 0xFFFFFFFF — crc32_compute);
|
|
* - the hashed byte range (the upload rect, row-major, X wrapped at 1024);
|
|
* - the rect a draw is matched against (the UV-limited sample rect, see
|
|
* tex_pack_on_textured_prim).
|
|
*
|
|
* Where this sits: uploads, fills, copies and textured draws are all observed
|
|
* through the gr_* renderer facade (gpu_render.c), so tracking is
|
|
* backend-agnostic — software, OpenGL and Vulkan feed the same tracker. This
|
|
* mirrors how the [[vram_patch]] layer in text_xlate.cpp rides the GP0(A0)
|
|
* completion hook.
|
|
*
|
|
* Lifetime model (v1): a tracked upload dies wholesale as soon as anything
|
|
* overwrites any part of its rect. Beetle instead splits a partially-overwritten
|
|
* rect into surviving subregions; that is deferred, and the dump-diff against a
|
|
* Beetle dump of the same play session is what measures the cost.
|
|
*/
|
|
#ifndef PSXRECOMP_TEX_PACK_H
|
|
#define PSXRECOMP_TEX_PACK_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* One-time init, called once the disc path is resolved.
|
|
*
|
|
* `disc_path` is the loaded .cue/.bin/.chd; the pack and dump folder names
|
|
* derive from its stem, matching Beetle's `retro_cd_base_name`. `dir_override`,
|
|
* when non-empty, replaces the disc's directory as the parent of both folders.
|
|
*
|
|
* `pack_dir`, when non-empty, is used DIRECTLY as the replacement folder and
|
|
* overrides that derivation entirely. This is the managed-pack path: a launcher
|
|
* keeps packs in its own store under an arbitrary id, and there is no reason to
|
|
* force such a folder to be named after the disc. When it is empty the Beetle
|
|
* drop-in convention applies unchanged, so a pack dropped next to the disc
|
|
* still just works. The dump folder always follows the disc-stem convention.
|
|
*
|
|
* Cheap no-op (and tex_pack_active() stays 0) when both flags are 0. */
|
|
/* Add a config-owned exclusion (call after tex_pack_init). */
|
|
void tex_pack_add_excluded(uint32_t hash);
|
|
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.
|
|
* The launcher reads this to show per-pack coverage without running the game. */
|
|
void tex_pack_write_coverage(void);
|
|
|
|
/* 1 when either replacement or dumping is on. The gr_* hooks test this first so
|
|
* a disabled build costs one predictable branch per primitive. */
|
|
int tex_pack_active(void);
|
|
|
|
/* ---- VRAM tracking (driven from the gr_* facade) ---- */
|
|
|
|
/* A completed CPU→VRAM image transfer (GP0 0xA0). `pixels` is the staged
|
|
* w*h halfword rectangle — exactly the buffer whose CRC32 is the texture hash. */
|
|
void tex_pack_on_upload(int x, int y, int w, int h, const uint16_t *pixels);
|
|
|
|
/* A VRAM→VRAM copy destination. Call before the backend mutates VRAM: this
|
|
* preserves the ordinary destination-invalidation ordering, then leaves a
|
|
* cheap unresolved candidate. The first textured draw that touches the copied
|
|
* rectangle asks for one exact backend readback through the two functions
|
|
* below, so copies that are never sampled as textures are never hashed. */
|
|
void tex_pack_on_copy(int src_x, int src_y, int dst_x, int dst_y,
|
|
int w, int h, int content_preserved);
|
|
|
|
/* Return one unresolved copy rectangle touched by this primitive, or 0. The
|
|
* caller reads that exact rectangle from the authoritative renderer backend
|
|
* and passes it to tex_pack_resolve_copy(). Once the first readback has made
|
|
* the backend's CPU mirror current, tex_pack_next_pending_copy() lets the
|
|
* caller drain the rest of the batch without another GPU synchronization. */
|
|
int tex_pack_pending_copy_for_prim(const int lim[4], uint16_t texpage,
|
|
int out_rect[4]);
|
|
int tex_pack_next_pending_copy(int out_rect[4]);
|
|
void tex_pack_resolve_copy(int x, int y, int w, int h,
|
|
const uint16_t *pixels);
|
|
|
|
/* Any non-upload/non-copy write to VRAM (fill or direct poke). Kills every
|
|
* tracked upload or unresolved copy the rect touches and drops stale CLUT
|
|
* hashes. */
|
|
void tex_pack_invalidate(int x, int y, int w, int h);
|
|
|
|
/* GP0(E2h) texture window, raw 20-bit parameter. */
|
|
void tex_pack_set_texture_window(uint32_t raw);
|
|
|
|
/* ---- per-primitive texture identity ---- */
|
|
|
|
/* Called for every textured primitive with its inclusive, post-wrap UV bounds
|
|
* (lim = {lo_u, lo_v, hi_u, hi_v}, as produced by psx_uv_tri_limits /
|
|
* psx_uv_rect_limits in gpu_uv.h) and the primitive's CLUT and texpage. */
|
|
void tex_pack_on_textured_prim(const int lim[4], uint16_t clut_x, uint16_t clut_y,
|
|
uint16_t texpage);
|
|
|
|
/* ---- replacement lookup (the substitution path) ---- */
|
|
|
|
/* A decoded replacement plus everything needed to address it from a primitive's
|
|
* texture-page UVs:
|
|
*
|
|
* s = (u - origin_u) / src_w t = (v - origin_v) / src_h
|
|
*
|
|
* i.e. the source texture occupies src_w x src_h TEXELS starting at
|
|
* (origin_u, origin_v) within the texture page, and the replacement image
|
|
* covers exactly that region at whatever resolution it happens to be. The
|
|
* caller never needs to know the upscale factor.
|
|
*
|
|
* `pixels` is RGBA8, width*height*4, owned by tex_pack and stable for the
|
|
* process lifetime. `gl_handle` points at a slot the backend may use to cache
|
|
* an uploaded texture name so it uploads each image once; tex_pack only ever
|
|
* reads it as an opaque value. `id` identifies the entry for that caching. */
|
|
typedef struct {
|
|
const unsigned char *pixels;
|
|
int width, height; /* replacement image, pixels */
|
|
int src_w, src_h; /* source texture, texels */
|
|
int origin_u, origin_v; /* source origin in the page, texels */
|
|
unsigned long long id;
|
|
unsigned *gl_handle;
|
|
/* 1 = this image was matched on TEXTURE hash alone because the pack ships
|
|
* no file for the draw's CLUT. It is a single-colour mask, so the shape is
|
|
* right but the colour is whatever palette variant happened to be on disk;
|
|
* the renderer must take the colour from the live CLUT instead. */
|
|
int recolour;
|
|
/* Valid only when recolour is set. ink_index is the palette entry this
|
|
* image inks, used for HD ink that overhangs a hole texel; ref_max is the
|
|
* image's own full-ink max channel in 0..1, which the shader divides by to
|
|
* recover the antialiasing ramp. */
|
|
int ink_index;
|
|
float ref_max;
|
|
} TexPackRepl;
|
|
|
|
/* 1 when this primitive has a replacement (decoding it on first use), 0
|
|
* otherwise. Cheap no-op unless replacement is enabled and the pack is
|
|
* non-empty. */
|
|
int tex_pack_lookup_replacement(const int lim[4], uint16_t clut_x, uint16_t clut_y,
|
|
uint16_t texpage, TexPackRepl *out);
|
|
|
|
/* Live enable/disable for substitution, independent of the pack being loaded.
|
|
* Exists so an A/B costs a command instead of a rebuild — every wrong guess
|
|
* about why replacement broke the HUD cost a full build-and-relaunch cycle,
|
|
* and half of those could have been settled in ten seconds. -1 = query. */
|
|
int tex_pack_replace_enabled(int set);
|
|
|
|
/* Census of primitives that were actually ARMED with a replacement, keyed by
|
|
* texture and carrying the primitive's SCREEN bounds.
|
|
*
|
|
* The counters say how many draws were replaced but not WHICH, and that is the
|
|
* open question: the HUD's own font atlases are never matched, yet the HUD
|
|
* disappears whenever substitution is on. If a HUD-region primitive shows up
|
|
* here, it is being drawn through some other texture's replacement image. */
|
|
void tex_pack_note_armed(unsigned long long id, int x0, int y0, int x1, int y1);
|
|
|
|
/* Debug-server surface. subcmd:
|
|
* "stats" -> counters + folder paths + pack/dump coverage
|
|
* "uploads" -> JSON array of the live tracked uploads {x,y,w,h,hash}
|
|
* "dumped" -> JSON array of the <hash>-<pal> keys written this session
|
|
* "missing" -> pack entries never matched by a draw yet (authoring aid)
|
|
* Returns bytes written into out (<= cap). */
|
|
int tex_pack_debug_json(const char *subcmd, char *out, int cap);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSXRECOMP_TEX_PACK_H */
|