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>
274 lines
10 KiB
C++
274 lines
10 KiB
C++
#include "crc32.h"
|
|
#include "tex_pack.h"
|
|
|
|
#include <cstdint>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
uint16_t s_vram[1024 * 512]{};
|
|
int s_failures = 0;
|
|
bool s_decode_enabled = false;
|
|
|
|
void check(bool ok, const char *what) {
|
|
if (ok) return;
|
|
std::fprintf(stderr, "tex_pack_copy_test: FAIL: %s\n", what);
|
|
++s_failures;
|
|
}
|
|
|
|
std::string debug_json(const char *subcmd) {
|
|
char out[8192]{};
|
|
const int n = tex_pack_debug_json(subcmd, out, (int)sizeof(out));
|
|
return std::string(out, out + n);
|
|
}
|
|
|
|
std::string expected_key(const uint16_t *pixels, size_t count,
|
|
int clut_x, int clut_y) {
|
|
const uint32_t tex = crc32_compute(reinterpret_cast<const uint8_t *>(pixels),
|
|
count * sizeof(uint16_t));
|
|
const uint32_t pal = crc32_compute(
|
|
reinterpret_cast<const uint8_t *>(s_vram + clut_y * 1024 + clut_x),
|
|
16 * sizeof(uint16_t));
|
|
char key[64];
|
|
std::snprintf(key, sizeof(key), "[\"%x-%x\"]", tex, pal);
|
|
return key;
|
|
}
|
|
|
|
std::string raw_key(const uint16_t *pixels, size_t count,
|
|
int clut_x, int clut_y) {
|
|
const std::string wrapped = expected_key(pixels, count, clut_x, clut_y);
|
|
return wrapped.substr(2, wrapped.size() - 4);
|
|
}
|
|
|
|
void init_dump(const std::filesystem::path &root) {
|
|
tex_pack_init((root / "copy-test.cue").string().c_str(), 0, 1,
|
|
root.string().c_str(), "");
|
|
}
|
|
|
|
void test_pack_key_parity(const std::filesystem::path &root) {
|
|
constexpr int clut_x = 32;
|
|
constexpr int clut_y = 400;
|
|
const uint16_t pixels[8] = {
|
|
0x3210, 0x7654, 0xBA98, 0xFEDC,
|
|
0x1110, 0x3332, 0x5554, 0x7776,
|
|
};
|
|
const int lim[4] = {0, 0, 15, 1}; /* 4 words x 2 rows at 4bpp */
|
|
const std::string expected = expected_key(pixels, 8, clut_x, clut_y);
|
|
|
|
init_dump(root);
|
|
tex_pack_on_upload(0, 0, 4, 2, pixels);
|
|
tex_pack_on_textured_prim(lim, clut_x, clut_y, 0);
|
|
const std::string upload_key = debug_json("dumped");
|
|
check(upload_key == expected, "CPU upload emits expected texture/palette key");
|
|
tex_pack_shutdown();
|
|
|
|
init_dump(root);
|
|
tex_pack_on_copy(100, 100, 64, 0, 4, 2, 1);
|
|
int rect[4]{};
|
|
check(tex_pack_pending_copy_for_prim(lim, 1, rect) == 1,
|
|
"copied destination becomes a pending texture candidate");
|
|
check(rect[0] == 64 && rect[1] == 0 && rect[2] == 4 && rect[3] == 2,
|
|
"pending readback preserves the complete GP0 copy rectangle");
|
|
tex_pack_resolve_copy(rect[0], rect[1], rect[2], rect[3], pixels);
|
|
tex_pack_on_textured_prim(lim, clut_x, clut_y, 1);
|
|
const std::string copy_key = debug_json("dumped");
|
|
check(copy_key == expected, "VRAM copy emits expected texture/palette key");
|
|
check(copy_key == upload_key, "VRAM copy and CPU upload pack keys are identical");
|
|
tex_pack_shutdown();
|
|
}
|
|
|
|
void test_copy_invalidation(const std::filesystem::path &root) {
|
|
const uint16_t source[8] = {
|
|
0x0123, 0x4567, 0x89AB, 0xCDEF,
|
|
0x0246, 0x1357, 0x8ACE, 0x9BDF,
|
|
};
|
|
const uint16_t stale[8] = {
|
|
0xFFFF, 0xEEEE, 0xDDDD, 0xCCCC,
|
|
0xBBBB, 0xAAAA, 0x9999, 0x8888,
|
|
};
|
|
|
|
init_dump(root);
|
|
tex_pack_on_upload(0, 0, 4, 2, source);
|
|
tex_pack_on_upload(64, 0, 4, 2, stale);
|
|
check(debug_json("uploads").find("\"x\":64") != std::string::npos,
|
|
"precondition: old destination upload is tracked");
|
|
|
|
tex_pack_on_copy(0, 0, 64, 0, 4, 2, 1);
|
|
const std::string after_begin = debug_json("uploads");
|
|
char stale_hash[32];
|
|
std::snprintf(stale_hash, sizeof(stale_hash), "\"hash\":\"%x\"",
|
|
crc32_compute(reinterpret_cast<const uint8_t *>(stale),
|
|
sizeof(stale)));
|
|
check(after_begin.find(stale_hash) == std::string::npos,
|
|
"copy invalidates the old destination before registering new bytes");
|
|
check(after_begin.find("\"x\":64") != std::string::npos,
|
|
"unmasked full-rect copy propagates the new destination immediately");
|
|
check(after_begin.find("\"x\":0") != std::string::npos,
|
|
"copy destination invalidation does not kill a disjoint source");
|
|
|
|
const int lim[4] = {0, 0, 15, 1};
|
|
int rect[4]{};
|
|
check(tex_pack_pending_copy_for_prim(lim, 1, rect) == 0,
|
|
"tracked full-rect source avoids a backend readback");
|
|
|
|
tex_pack_invalidate(65, 0, 1, 1);
|
|
const std::string after_partial_write = debug_json("uploads");
|
|
check(after_partial_write.find("\"x\":64") == std::string::npos,
|
|
"partial overwrite still kills the entire copied upload");
|
|
check(after_partial_write.find("\"x\":0") != std::string::npos,
|
|
"partial destination overwrite leaves disjoint uploads intact");
|
|
tex_pack_shutdown();
|
|
}
|
|
|
|
void test_copy_is_replaceable(const std::filesystem::path &root) {
|
|
constexpr int clut_x = 32;
|
|
constexpr int clut_y = 400;
|
|
const uint16_t pixels[8] = {
|
|
0x3210, 0x7654, 0xBA98, 0xFEDC,
|
|
0x1110, 0x3332, 0x5554, 0x7776,
|
|
};
|
|
const int lim[4] = {0, 0, 15, 1};
|
|
const std::string key = raw_key(pixels, 8, clut_x, clut_y);
|
|
const std::filesystem::path pack = root / "replacement-pack";
|
|
std::filesystem::create_directories(pack);
|
|
std::ofstream(pack / (key + ".png"), std::ios::binary).put('\0');
|
|
|
|
tex_pack_init((root / "copy-test.cue").string().c_str(), 1, 0,
|
|
root.string().c_str(), pack.string().c_str());
|
|
tex_pack_on_copy(100, 100, 64, 0, 4, 2, 1);
|
|
int rect[4]{};
|
|
check(tex_pack_pending_copy_for_prim(lim, 1, rect) == 1,
|
|
"indexed replacement keeps a copied destination pending");
|
|
tex_pack_resolve_copy(64, 0, 4, 2, pixels);
|
|
check(debug_json("uploads").find("\"x\":64") != std::string::npos,
|
|
"copy with an indexed texture hash becomes a replacement candidate");
|
|
|
|
s_decode_enabled = true;
|
|
TexPackRepl repl{};
|
|
check(tex_pack_lookup_replacement(lim, clut_x, clut_y, 1, &repl) == 1,
|
|
"copied destination resolves through the normal replacement lookup");
|
|
check(repl.id == ((unsigned long long)crc32_compute(
|
|
reinterpret_cast<const uint8_t *>(pixels),
|
|
sizeof(pixels)) << 32 |
|
|
crc32_compute(reinterpret_cast<const uint8_t *>(
|
|
s_vram + clut_y * 1024 + clut_x),
|
|
16 * sizeof(uint16_t))),
|
|
"replacement lookup retains the copied texture/palette key");
|
|
s_decode_enabled = false;
|
|
tex_pack_shutdown();
|
|
}
|
|
|
|
void benchmark_copy_fast_path(const std::filesystem::path &root) {
|
|
std::vector<uint16_t> tile(64 * 64);
|
|
for (size_t i = 0; i < tile.size(); ++i)
|
|
tile[i] = (uint16_t)(i * 109u + 17u);
|
|
|
|
init_dump(root);
|
|
tex_pack_on_upload(0, 0, 64, 64, tile.data());
|
|
for (int i = 0; i < 1000; ++i)
|
|
tex_pack_on_copy(0, 0, 128, 0, 64, 64, 1);
|
|
|
|
constexpr int copies = 200000;
|
|
const auto copy_start = std::chrono::steady_clock::now();
|
|
for (int i = 0; i < copies; ++i)
|
|
tex_pack_on_copy(0, 0, 128, 0, 64, 64, 1);
|
|
const auto copy_end = std::chrono::steady_clock::now();
|
|
|
|
const int lim[4] = {0, 0, 15, 15};
|
|
int rect[4]{};
|
|
constexpr int queries = 5000000;
|
|
int query_sum = 0;
|
|
const auto query_start = std::chrono::steady_clock::now();
|
|
for (int i = 0; i < queries; ++i)
|
|
query_sum += tex_pack_pending_copy_for_prim(lim, 0, rect);
|
|
const auto query_end = std::chrono::steady_clock::now();
|
|
|
|
constexpr int hashes = 50000;
|
|
uint32_t hash_sum = 0;
|
|
const auto hash_start = std::chrono::steady_clock::now();
|
|
for (int i = 0; i < hashes; ++i)
|
|
hash_sum ^= crc32_compute(reinterpret_cast<const uint8_t *>(tile.data()),
|
|
tile.size() * sizeof(uint16_t));
|
|
const auto hash_end = std::chrono::steady_clock::now();
|
|
|
|
const auto ns_copy = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
copy_end - copy_start).count() / copies;
|
|
const auto ns_query = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
query_end - query_start).count() / queries;
|
|
const auto ns_hash = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
hash_end - hash_start).count() / hashes;
|
|
std::printf("tex_pack_copy_bench: tracked 64x64 copy=%lld ns, "
|
|
"no-pending draw query=%lld ns, 64x64 CRC=%lld ns, guard=%d/%u\n",
|
|
(long long)ns_copy, (long long)ns_query, (long long)ns_hash,
|
|
query_sum, hash_sum);
|
|
tex_pack_shutdown();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" const uint16_t *gpu_get_vram(void) {
|
|
return s_vram;
|
|
}
|
|
|
|
/* tex_pack.cpp only reaches image decode when replacement lookup opens a pack
|
|
* file. These focused dump/tracking tests intentionally never do that. */
|
|
extern "C" unsigned char *stbi_load_from_memory(const unsigned char *, int,
|
|
int *x, int *y, int *comp,
|
|
int) {
|
|
if (!s_decode_enabled) return nullptr;
|
|
*x = 16;
|
|
*y = 2;
|
|
*comp = 4;
|
|
auto *pixels = static_cast<unsigned char *>(std::malloc(16 * 2 * 4));
|
|
if (!pixels) return nullptr;
|
|
for (int i = 0; i < 16 * 2; ++i) {
|
|
pixels[i * 4 + 0] = (unsigned char)(32 + i);
|
|
pixels[i * 4 + 1] = (unsigned char)(96 + i);
|
|
pixels[i * 4 + 2] = (unsigned char)(160 + i);
|
|
pixels[i * 4 + 3] = 255;
|
|
}
|
|
return pixels;
|
|
}
|
|
|
|
extern "C" void stbi_image_free(void *pixels) {
|
|
std::free(pixels);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
for (int i = 0; i < 16; ++i)
|
|
s_vram[400 * 1024 + 32 + i] = (uint16_t)(0x0421u * (uint16_t)i);
|
|
|
|
const auto stamp = std::to_string(
|
|
(unsigned long long)std::filesystem::file_time_type::clock::now()
|
|
.time_since_epoch().count());
|
|
const std::filesystem::path root =
|
|
std::filesystem::temp_directory_path() / ("tex_pack_copy_test_" + stamp);
|
|
std::filesystem::create_directories(root);
|
|
|
|
if (argc > 1 && std::string(argv[1]) == "--benchmark") {
|
|
benchmark_copy_fast_path(root);
|
|
} else {
|
|
test_pack_key_parity(root);
|
|
test_copy_invalidation(root);
|
|
test_copy_is_replaceable(root);
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(root, ec);
|
|
if (ec) {
|
|
std::fprintf(stderr, "tex_pack_copy_test: cleanup warning: %s\n",
|
|
ec.message().c_str());
|
|
}
|
|
|
|
if (s_failures) return 1;
|
|
std::printf("tex_pack_copy_test: PASS\n");
|
|
return 0;
|
|
}
|