mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
8630d3a6f8
With every drawn vertex precise, a title can still show cracks where the game
culled a face on its integer winding. R4's road emitter runs NCLIP on each
far row and skips rows whose native height rounds to 0. Natively their
neighbours meet at the same integer y. With PGXP they sit at their exact y,
and the skipped row becomes a strip of sky across the road beyond the start
gantry, at 4K and in 2P. tri_mixed cannot see this: the missing face draws
no triangle.
Precise culling (DuckStation's "PGXP culling", minimal footprint):
- While geometry correction is armed, gte_nclip computes the exact
determinant of the three SXY register shadows (pgxp_gte_nclip_precise:
live, current words, the GPU's own truncation agreement and tolerance).
When its sign is nonzero and differs from the integer MAC0's, MAC0 takes
that sign, with the rounded exact doubled area (at least 1) as magnitude.
FLAG keeps the integer overflow bits.
- It changes guest-visible MAC0, so it is a `culling` option of
psx.enhancement.pgxp, false in the builtin, with no [video] key, so netplay
(which clears every mod) never runs it. It needs geometry correction.
It is held off in every pass compared against another execution: the
speculative and replay passes, and the whole overlay shadow diff
(psx_overlay_shadow_diff_active), interpreter pass included.
- Counters whenever geometry correction is armed: nclip_precise,
nclip_disagree (exact sign differs: the culled-face crack exposure),
nclip_corrected. TCP pgxp {"culling":0|1}; env PSX_PGXP_CULLING.
Session arming in one place (src/pgxp_session.cpp, psx_pgxp_session_arm):
main.cpp's renderer setup now calls the same code the test drives. The arming
takes the mod request (reads and clears it), so a session with an empty plan
cannot inherit an earlier session's request even if the reset were skipped.
pgxp_session_test now links the real mod runtime, the builtin plugin and the
builtin manifest, and covers:
- a default-on override;
- a netplay clear, with and without the reset;
- the off switch;
- the options;
- env overrides;
- pgxp_mod_only.
[video] pgxp_mod_only (game.toml, default false): for a title that ships
PGXP through the mod, the mod is the one switch. The [video]
geometry_correction / perspective_texturing / pgxp_cpu_mode values are not
applied, from game.toml or the player's settings.toml. The launcher also
hides its Perspective textures row (has_geometry_precision = 0). That row
showed "off" while a default-on mod corrected textures, and it could keep
texture correction on after the player switched the mod off.
geom_correction also reports texture_correction and the culling switch.
R4, Helter Skelter: 28.7% of NCLIPs disagree in sign. With culling, the
ladder is gone in same-frame 4K captures (1P straight and crest, 2P VS), and
the game emits about 24% more triangles. Its packet buffers peak at 60%.
fp_identity over 12000 frames: stock, PGXP off, and PGXP on with culling off
are IDENTICAL; with culling on, the RAM writes differ from the attract race
on, while cyc/mmio/mc agree throughout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
321 lines
13 KiB
C++
321 lines
13 KiB
C++
/* test_pgxp_session.cpp -- does the psx.enhancement.pgxp mod arm PGXP, and
|
|
* only for the session whose plan holds it? (docs/ENHANCEMENTS.md G1.11/G1.12)
|
|
*
|
|
* The mod's activation runs at session start; main.cpp's renderer setup runs
|
|
* after it and applies the [video] baseline. Before G1.11 the activation
|
|
* armed the corrections directly and the baseline switched them straight back
|
|
* off, so the mod did nothing. Now the activation records a request and the
|
|
* session arming (src/pgxp_session.cpp, the code main.cpp calls) takes it.
|
|
*
|
|
* This test links the real mod runtime and package manager, the real builtin
|
|
* plugin (src/mod_builtin_pgxp.c, registered by its constructor) and the real
|
|
* arming, stages the framework's own builtin manifest (and a title override
|
|
* of it with default_enabled = true, as a title that ships PGXP on by default
|
|
* does), and drives sessions in main.cpp's order:
|
|
*
|
|
* commit (offline) or mod_runtime_clear_for_netplay (netplay)
|
|
* reset_mod_owned_presentation() -> psx_pgxp_session_reset()
|
|
* mod_runtime_activate_plugins() -> builtin_pgxp_activate (if planned)
|
|
* renderer setup -> psx_pgxp_session_arm(...)
|
|
*
|
|
* The engine setters the arming calls are stubbed to record what it armed.
|
|
*/
|
|
#include "mod_runtime.h"
|
|
#include "mod_packages.h"
|
|
#include "mod_plugins.h"
|
|
#include "pgxp.h"
|
|
#include "pgxp_session.h"
|
|
|
|
#include "cpu_state.h"
|
|
#include "gpu.h"
|
|
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#ifndef PSX_BUILTIN_PGXP_MANIFEST
|
|
#error "PSX_BUILTIN_PGXP_MANIFEST must name mods/builtin/.../manifest.toml"
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
static int g_failures = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \
|
|
#cond); \
|
|
g_failures++; \
|
|
} \
|
|
} while (0)
|
|
|
|
/* ---- what the arming armed (gte.cpp / gpu.c setters, stubbed) ------------ */
|
|
|
|
static int g_geometry = -1;
|
|
static int g_texture = -1;
|
|
extern "C" void gte_geometry_correction_set(int enabled) { g_geometry = enabled; }
|
|
extern "C" void gpu_texture_correction_set(int enabled) { g_texture = enabled; }
|
|
/* pgxp.cpp's position-cache tier lives in gte.cpp. */
|
|
extern "C" int gte_geometry_correction_lookup(uint32_t, int32_t*, int32_t*) {
|
|
return 0;
|
|
}
|
|
|
|
/* ---- the runtime surface mod_runtime.cpp links against ------------------- */
|
|
|
|
static std::array<uint8_t, 2 * 1024 * 1024> ram;
|
|
extern "C" uint8_t psx_read_byte(uint32_t a) { return ram[a & 0x1fffffu]; }
|
|
extern "C" void psx_write_byte(uint32_t a, uint8_t v) { ram[a & 0x1fffffu] = v; }
|
|
extern "C" uint16_t psx_read_half(uint32_t a) {
|
|
const uint32_t o = a & 0x1fffffu;
|
|
return (uint16_t)(ram[o] | ((uint16_t)ram[o + 1] << 8));
|
|
}
|
|
extern "C" void psx_write_half(uint32_t a, uint16_t v) {
|
|
const uint32_t o = a & 0x1fffffu;
|
|
ram[o] = (uint8_t)v;
|
|
ram[o + 1] = (uint8_t)(v >> 8);
|
|
}
|
|
extern "C" uint32_t psx_read_word(uint32_t a) {
|
|
const uint32_t o = a & 0x1fffffu;
|
|
return (uint32_t)ram[o] | ((uint32_t)ram[o + 1] << 8) |
|
|
((uint32_t)ram[o + 2] << 16) | ((uint32_t)ram[o + 3] << 24);
|
|
}
|
|
extern "C" void psx_write_word(uint32_t a, uint32_t v) {
|
|
const uint32_t o = a & 0x1fffffu;
|
|
for (int i = 0; i < 4; i++) ram[o + i] = (uint8_t)(v >> (8 * i));
|
|
}
|
|
extern "C" void psx_host_write_byte(uint32_t a, uint8_t v) { psx_write_byte(a, v); }
|
|
extern "C" void psx_host_write_half(uint32_t a, uint16_t v) { psx_write_half(a, v); }
|
|
extern "C" void psx_host_write_word(uint32_t a, uint32_t v) { psx_write_word(a, v); }
|
|
extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; }
|
|
extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) { return 0; }
|
|
extern "C" int psx_ws_x_margin(void) { return 0; }
|
|
extern "C" void gpu_get_display_info(GpuDisplayInfo* out) { *out = GpuDisplayInfo{}; }
|
|
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
|
|
extern "C" int fntrace_is_game_started(void) { return 1; }
|
|
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
|
|
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
|
|
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
|
|
|
|
/* ---- staging ------------------------------------------------------------- */
|
|
|
|
static void write_text(const fs::path& path, const std::string& text) {
|
|
fs::create_directories(path.parent_path());
|
|
std::ofstream out(path, std::ios::binary);
|
|
out << text;
|
|
}
|
|
|
|
static std::string read_text(const fs::path& path) {
|
|
std::ifstream in(path, std::ios::binary);
|
|
std::ostringstream ss;
|
|
ss << in.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
static std::string replace_once(std::string s, const std::string& from,
|
|
const std::string& to) {
|
|
const size_t at = s.find(from);
|
|
CHECK(at != std::string::npos);
|
|
if (at != std::string::npos) s.replace(at, from.size(), to);
|
|
return s;
|
|
}
|
|
|
|
/* A mods root holding the builtin manifest as `manifest` (verbatim, or a
|
|
* title's default-on override of it) and an optional state.toml. */
|
|
static fs::path stage(const char* name, const std::string& manifest,
|
|
const std::string& state) {
|
|
const fs::path root = fs::temp_directory_path() /
|
|
(std::string("psxrecomp-pgxp-session-") + name);
|
|
std::error_code ec;
|
|
fs::remove_all(root, ec);
|
|
write_text(root / "packages/psx.enhancement.pgxp/1.0.0/manifest.toml",
|
|
manifest);
|
|
if (!state.empty()) write_text(root / "state.toml", state);
|
|
std::vector<char> zeros(8 * 2352, 0);
|
|
write_text(root / "disc.bin", std::string(zeros.begin(), zeros.end()));
|
|
write_text(root / "disc.cue",
|
|
"FILE \"disc.bin\" BINARY\n"
|
|
" TRACK 01 MODE2/2352\n"
|
|
" INDEX 01 00:00:00\n");
|
|
return root;
|
|
}
|
|
|
|
static bool commit(const fs::path& root) {
|
|
std::string error;
|
|
if (!PSXRecompV4::mod_runtime_initialize(root, "SLUS-PGXPT", 0x80010000u,
|
|
{}, &error)) {
|
|
std::fprintf(stderr, "initialize: %s\n", error.c_str());
|
|
return false;
|
|
}
|
|
if (!PSXRecompV4::mod_runtime_commit(root / "disc.cue", &error)) {
|
|
std::fprintf(stderr, "commit: %s\n", error.c_str());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ---- one session start, as main.cpp sequences it ------------------------- */
|
|
|
|
static PSXPgxpSessionConfig g_cfg;
|
|
|
|
static PSXPgxpSessionArm session(bool do_reset = true) {
|
|
if (do_reset) psx_pgxp_session_reset(); /* reset_mod_owned_presentation */
|
|
mod_runtime_activate_plugins();
|
|
g_geometry = g_texture = -1;
|
|
return psx_pgxp_session_arm(&g_cfg, nullptr);
|
|
}
|
|
|
|
static bool armed(const PSXPgxpSessionArm& a, int g, int t, int c, int cull) {
|
|
return a.geometry == g && a.texture == t && a.cpu_mode == c &&
|
|
a.culling == cull && g_geometry == g && g_texture == t &&
|
|
pgxp_cpu_mode() == c && pgxp_culling() == cull;
|
|
}
|
|
|
|
static void netplay_clear(void) {
|
|
std::string error;
|
|
CHECK(PSXRecompV4::mod_runtime_clear_for_netplay(&error));
|
|
}
|
|
|
|
int main(void) {
|
|
for (const char* v : {"PSX_GEOMETRY_CORRECTION", "PSX_PERSPECTIVE_TEXTURING",
|
|
"PSX_PGXP_CPU_MODE", "PSX_PGXP_CULLING"})
|
|
unsetenv(v);
|
|
g_cfg = PSXPgxpSessionConfig{};
|
|
g_cfg.tolerance = -1.0f;
|
|
g_cfg.position_fallback = 0;
|
|
g_cfg.preserve_projection = 1;
|
|
|
|
const std::string builtin = read_text(PSX_BUILTIN_PGXP_MANIFEST);
|
|
CHECK(builtin.find("id = \"psx.enhancement.pgxp\"") != std::string::npos);
|
|
CHECK(builtin.find("default_enabled = false") != std::string::npos);
|
|
CHECK(builtin.find("id = \"culling\"") != std::string::npos);
|
|
/* A title that ships PGXP on by default overrides the builtin at the
|
|
* same id and version: the feature on, and here precise culling too. */
|
|
std::string title = replace_once(builtin, "default_enabled = false",
|
|
"default_enabled = true");
|
|
{
|
|
const size_t opt = title.find("id = \"culling\"");
|
|
const size_t def = title.find("default = \"false\"", opt);
|
|
CHECK(opt != std::string::npos && def != std::string::npos);
|
|
if (def != std::string::npos)
|
|
title.replace(def, std::strlen("default = \"false\""),
|
|
"default = \"true\"");
|
|
}
|
|
|
|
/* The builtin's default: nothing planned, the faithful floor, and the
|
|
* tuning keys are applied regardless (they are inert while off). */
|
|
CHECK(commit(stage("builtin", builtin, "")));
|
|
CHECK(armed(session(), 0, 0, 0, 0));
|
|
CHECK(pgxp_tolerance() < 0.0f && pgxp_position_fallback() == 0 &&
|
|
pgxp_preserve_projection() == 1);
|
|
|
|
/* A title's default-on override: the mod arms geometry, texture and its
|
|
* culling option at the first boot (the G1.11 bug left it all off). */
|
|
const fs::path title_root = stage("title", title, "");
|
|
CHECK(commit(title_root));
|
|
CHECK(armed(session(), 1, 1, 0, 1));
|
|
|
|
/* Netplay rematch: the plan is cleared, nothing activates, PGXP is off
|
|
* -- default-on packages included. */
|
|
netplay_clear();
|
|
CHECK(armed(session(), 0, 0, 0, 0));
|
|
/* ...even if the session-start reset were skipped: the offline
|
|
* session's arming took its request, so nothing is left to inherit. */
|
|
CHECK(commit(title_root));
|
|
CHECK(armed(session(), 1, 1, 0, 1));
|
|
netplay_clear();
|
|
CHECK(armed(session(false), 0, 0, 0, 0));
|
|
|
|
/* Offline rematch after netplay: on again. */
|
|
CHECK(commit(title_root));
|
|
CHECK(armed(session(), 1, 1, 0, 1));
|
|
|
|
/* The player's off switch on the Mods page. */
|
|
const std::string disabled =
|
|
"format_version = 2\n"
|
|
"[[package]]\n"
|
|
"id = \"psx.enhancement.pgxp\"\n"
|
|
"version = \"1.0.0\"\n"
|
|
"[[feature]]\n"
|
|
"package_id = \"psx.enhancement.pgxp\"\n"
|
|
"id = \"pgxp\"\n"
|
|
"enabled = false\n";
|
|
CHECK(commit(stage("title-off", title, disabled)));
|
|
CHECK(armed(session(), 0, 0, 0, 0));
|
|
|
|
/* Options: culling off, CPU mode on. */
|
|
const std::string options =
|
|
"format_version = 2\n"
|
|
"[[package]]\n"
|
|
"id = \"psx.enhancement.pgxp\"\n"
|
|
"version = \"1.0.0\"\n"
|
|
"[[feature]]\n"
|
|
"package_id = \"psx.enhancement.pgxp\"\n"
|
|
"id = \"pgxp\"\n"
|
|
"enabled = true\n"
|
|
"[feature.values]\n"
|
|
"culling = false\n"
|
|
"cpu_mode = true\n";
|
|
CHECK(commit(stage("title-options", title, options)));
|
|
CHECK(armed(session(), 1, 1, 1, 0));
|
|
|
|
/* [video] baseline: a player's keys work without the mod and add to it;
|
|
* culling has no [video] key. */
|
|
CHECK(commit(stage("title-off2", title, disabled)));
|
|
g_cfg.video_texture = 1;
|
|
CHECK(armed(session(), 0, 1, 0, 0));
|
|
g_cfg.video_geometry = 1;
|
|
CHECK(armed(session(), 1, 1, 0, 0));
|
|
/* ...unless the title made the mod the one switch (pgxp_mod_only): then
|
|
* a stale settings.toml value cannot keep half of PGXP on. */
|
|
g_cfg.mod_only = 1;
|
|
CHECK(armed(session(), 0, 0, 0, 0));
|
|
CHECK(commit(title_root));
|
|
CHECK(armed(session(), 1, 1, 0, 1));
|
|
netplay_clear();
|
|
CHECK(armed(session(), 0, 0, 0, 0));
|
|
g_cfg.mod_only = 0;
|
|
g_cfg.video_texture = 0;
|
|
g_cfg.video_geometry = 0;
|
|
|
|
/* Validation env overrides win over both. Culling needs geometry. */
|
|
CHECK(commit(title_root));
|
|
setenv("PSX_GEOMETRY_CORRECTION", "0", 1);
|
|
setenv("PSX_PERSPECTIVE_TEXTURING", "0", 1);
|
|
CHECK(armed(session(), 0, 0, 0, 0));
|
|
setenv("PSX_PERSPECTIVE_TEXTURING", "1", 1);
|
|
CHECK(armed(session(), 0, 1, 0, 0));
|
|
unsetenv("PSX_GEOMETRY_CORRECTION");
|
|
unsetenv("PSX_PERSPECTIVE_TEXTURING");
|
|
setenv("PSX_PGXP_CULLING", "0", 1);
|
|
CHECK(armed(session(), 1, 1, 0, 0));
|
|
unsetenv("PSX_PGXP_CULLING");
|
|
CHECK(commit(stage("builtin2", builtin, "")));
|
|
setenv("PSX_GEOMETRY_CORRECTION", "1", 1);
|
|
setenv("PSX_PGXP_CULLING", "1", 1);
|
|
CHECK(armed(session(), 1, 0, 0, 1));
|
|
setenv("PSX_PERSPECTIVE_TEXTURING", "", 1); /* set but empty = off */
|
|
CHECK(armed(session(), 1, 0, 0, 1));
|
|
unsetenv("PSX_GEOMETRY_CORRECTION");
|
|
unsetenv("PSX_PGXP_CULLING");
|
|
unsetenv("PSX_PERSPECTIVE_TEXTURING");
|
|
|
|
CHECK(psx_pgxp_session_env_flag(nullptr) == -1);
|
|
CHECK(psx_pgxp_session_env_flag("0") == 0);
|
|
CHECK(psx_pgxp_session_env_flag("") == 0);
|
|
CHECK(psx_pgxp_session_env_flag("1") == 1);
|
|
CHECK(psx_pgxp_session_env_flag("yes") == 1);
|
|
|
|
if (g_failures) {
|
|
std::fprintf(stderr, "test_pgxp_session: %d FAILURES\n", g_failures);
|
|
return 1;
|
|
}
|
|
std::printf("test_pgxp_session: all checks passed\n");
|
|
return 0;
|
|
}
|