diff --git a/mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml b/mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml index 8f0e2952..9bfd3a87 100644 --- a/mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml +++ b/mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml @@ -3,7 +3,7 @@ id = "psx.enhancement.pgxp" version = "1.0.0" name = "PGXP Precision (geometry + textures)" author = "psxrecomp" -description = "Sub-pixel vertex precision and perspective-correct texturing (the PGXP technique): the runtime shadows the GTE's full-precision projections and follows them to the GPU, so 3D polygons stop wobbling between whole pixels and large floor/wall textures stop warping. Visual-only: the game's own math, culling and readback see exactly what real hardware produces." +description = "Sub-pixel vertex precision and perspective-correct texturing (the PGXP technique): the runtime shadows the GTE's full-precision projections and follows them to the GPU, so 3D polygons stop wobbling between whole pixels and large floor/wall textures stop warping. Visual-only unless its precise culling option is on: the game's own math, culling and readback see exactly what real hardware produces." resolver = "declarative" save_compatibility = "shared" @@ -28,6 +28,15 @@ group = "Visual" type = "boolean" default = "false" +[[option]] +feature = "pgxp" +id = "culling" +label = "Precise culling" +description = "Also decide which faces the game culls from the same full-precision positions that PGXP draws, so distant rows the hardware rounds to zero height are no longer dropped and leave no gaps between their neighbours. Unlike the rest of PGXP this changes what the game itself computes (its NCLIP results), so it is off unless a game turns it on; netplay always runs without it." +group = "Visual" +type = "boolean" +default = "false" + [[plugin]] feature = "pgxp" id = "psx.pgxp" diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index baba72de..f44b54d0 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -686,6 +686,9 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& rt.video_pgxp_preserve_projection = toml::find(video, "pgxp_preserve_projection"); } + if (video.contains("pgxp_mod_only")) { + rt.video_pgxp_mod_only = toml::find(video, "pgxp_mod_only"); + } if (video.contains("crt_filter")) { const auto mode = toml::find(video, "crt_filter"); if (mode == "raw") rt.video_screen_kind = 0; diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 4789f856..309b6638 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -507,6 +507,17 @@ struct RuntimeConfig { // over TCP (pgxp verb). docs/ENHANCEMENTS.md G1.11. bool video_pgxp_preserve_projection = false; + // pgxp_mod_only: the title ships PGXP through the psx.enhancement.pgxp + // mod (typically a default-on override of it), which is then the one + // switch. The [video] geometry_correction / perspective_texturing / + // pgxp_cpu_mode values -- game.toml's and the player's settings.toml -- + // are not applied, and the launcher hides its Perspective textures row, + // so the player is never shown a second control that does nothing or + // keeps half of PGXP on after they switch the mod off. Netplay, which + // clears the mod, then runs with PGXP fully off. Default false + // (unchanged behaviour). game.toml only. docs/ENHANCEMENTS.md G1.12. + bool video_pgxp_mod_only = false; + // offer_vulkan: expose the experimental Vulkan renderer in the launcher. // Defaults false even for Vulkan-enabled builds; developers must opt in per // game once visuals are validated. diff --git a/recompiler/tests/video_enhancement_settings_test.cpp b/recompiler/tests/video_enhancement_settings_test.cpp index 0dc04d1c..e5dd7101 100644 --- a/recompiler/tests/video_enhancement_settings_test.cpp +++ b/recompiler/tests/video_enhancement_settings_test.cpp @@ -290,14 +290,18 @@ static void test_pgxp_title_keys() { "pgxp_position_fallback defaults ON (unchanged behaviour)"); check(!gc.runtime.video_pgxp_preserve_projection, "pgxp_preserve_projection defaults OFF (unchanged behaviour)"); + check(!gc.runtime.video_pgxp_mod_only, + "pgxp_mod_only defaults OFF (unchanged behaviour)"); fs::remove(p); p = write_game_toml("psxrecomp_pgxp_keys_dataflow.toml", "[video]\n" "pgxp_tolerance = -1.0\n" "pgxp_position_fallback = false\n" - "pgxp_preserve_projection = true\n"); + "pgxp_preserve_projection = true\n" + "pgxp_mod_only = true\n"); gc = PSXRecompV4::load_game_config(p); + check(gc.runtime.video_pgxp_mod_only, "pgxp_mod_only = true is honoured"); check(gc.runtime.video_pgxp_tolerance < 0.0, "pgxp_tolerance = -1.0 disables the clamp"); check(!gc.runtime.video_pgxp_position_fallback, @@ -316,6 +320,14 @@ static void test_pgxp_title_keys() { try { (void)PSXRecompV4::load_game_config(p); } catch (const std::exception&) { rejected = true; } check(rejected, "pgxp_preserve_projection must be a boolean"); fs::remove(p); + + p = write_game_toml("psxrecomp_pgxp_keys_bad_mod_only.toml", + "[video]\n" + "pgxp_mod_only = \"yes\"\n"); + rejected = false; + try { (void)PSXRecompV4::load_game_config(p); } catch (const std::exception&) { rejected = true; } + check(rejected, "pgxp_mod_only must be a boolean"); + fs::remove(p); } int main() { diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 61712ce7..a1e96cf4 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -378,9 +378,34 @@ if(BUILD_TESTING) # resolver main.cpp's renderer setup uses, in main.cpp's order. Pins that # the [video] baseline no longer undoes the mod, that a cleared (netplay) # plan keeps the baseline, and that the env overrides win. + # The session arming main.cpp runs (src/pgxp_session.cpp) against the + # real mod runtime, the real builtin plugin and the builtin's own + # manifest: default-on, netplay clear, the off switch, options, env + # overrides and [video] pgxp_mod_only. add_executable(pgxp_session_test tests/test_pgxp_session.cpp - src/mod_builtin_pgxp.c src/pgxp.cpp src/psx_ram_geometry.c) - target_include_directories(pgxp_session_test PRIVATE include) + src/pgxp_session.cpp src/mod_builtin_pgxp.c src/pgxp.cpp + src/psx_ram_geometry.c + src/mod_runtime.cpp + src/mod_packages.cpp + src/disc_path.cpp + src/cue_sheet.cpp + src/iso_reader.cpp + src/crc32.c + src/psx_sha256.c) + target_include_directories(pgxp_session_test PRIVATE + include + ../recompiler/include + ../recompiler/lib/toml11) + target_compile_definitions(pgxp_session_test PRIVATE + PSX_BUILTIN_PGXP_MANIFEST="${CMAKE_CURRENT_SOURCE_DIR}/../mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml") + target_link_libraries(pgxp_session_test PRIVATE chdr-static) + if(WIN32) + if(MSVC) + target_link_options(pgxp_session_test PRIVATE /STACK:16777216) + else() + target_link_options(pgxp_session_test PRIVATE -Wl,--stack,16777216) + endif() + endif() add_test(NAME pgxp_session_test COMMAND pgxp_session_test) add_executable(psx_instr_cost_test tests/test_psx_instr_cost.cpp) diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h index ea8ea7b2..86f46502 100644 --- a/runtime/include/pgxp.h +++ b/runtime/include/pgxp.h @@ -104,20 +104,47 @@ int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3, int shift, * ppp_window_fallback on reject. */ int pgxp_ppp_accept(int32_t x16, int32_t y16, uint32_t packed); +/* Precise culling (docs/ENHANCEMENTS.md G1.12). Off (the default): NCLIP is + * exactly the hardware's. On, while geometry correction is armed: when the + * three SXY FIFO shadows are live, describe the current register words and + * pass the same acceptance the GPU applies when it draws them, and the sign + * of their exact determinant differs from the integer one, NCLIP's MAC0 + * takes the exact sign (magnitude: the exact doubled area rounded, at least + * 1). A game that culls on that sign then keeps the sub-pixel faces PGXP + * draws with positive area -- the far road rows a native zero-area test + * drops, which open gaps between their precisely placed neighbours -- and + * drops the slivers whose drawn winding is reversed. This CHANGES + * guest-visible MAC0 and so guest control flow: only the mod arms it + * (netplay clears the mod), and gte.cpp holds it off in every pass that is + * compared against another execution (overlay shadow diff, speculative and + * replay passes). Live-tunable over TCP. */ +void pgxp_set_culling(int enabled); +int pgxp_culling(void); + +/* NCLIP's exact determinant from the SXY0..2 register shadows, in 2^-32 + * px^2 units (16.16 times 16.16). Returns 1 when all three shadows are live, + * carry the register words in sxy[] and pass the GPU consumer's truncation + * agreement and tolerance clamp; else 0. Counts nclip_precise. */ +int pgxp_gte_nclip_precise(const uint32_t sxy[3], int64_t *cross); + /* Mod-owned request (the framework's psx.enhancement.pgxp package). * * The mod's activation runs at session start, before main.cpp's renderer * setup, and that setup applies the [video] baseline (geometry_correction, * perspective_texturing, pgxp_cpu_mode). An activation that armed the * corrections directly was switched straight back off there. So the - * activation only records a request, and the renderer setup combines it with - * the baseline (pgxp_session.h). reset_mod_owned_presentation() clears it at - * every session start, so a session with an empty plan (netplay, or the mod - * disabled) gets exactly the [video] values. */ -void pgxp_mod_request(int enabled, int cpu_mode); -/* Returns nonzero when this session's plan asked for PGXP; *cpu_mode (may be - * NULL) receives the mod's CPU-mode option. */ -int pgxp_mod_requested(int *cpu_mode); + * activation only records a request, and the session arming + * (psx_pgxp_session_arm, pgxp_session.h) takes it -- reads and clears it -- + * and combines it with the baseline. A request therefore lives only from one + * session's activation to that session's arming: a later session whose plan + * is empty (netplay, or the mod disabled) cannot inherit it. + * reset_mod_owned_presentation() also clears it at every session start. */ +void pgxp_mod_request(int enabled, int cpu_mode, int culling); +/* Returns nonzero when this session's plan asked for PGXP; *cpu_mode and + * *culling (either may be NULL) receive the mod's options. */ +int pgxp_mod_requested(int *cpu_mode, int *culling); +/* pgxp_mod_requested, then clears the request. */ +int pgxp_mod_request_take(int *cpu_mode, int *culling); /* Drop all shadows (savestate load, raw RAM restore, timeline breaks). * O(1) via generation bump. Deferred while suppressed. */ @@ -207,6 +234,15 @@ typedef struct PGXPStats { * which keep the shortcut (drawing them as triangles would mix). */ uint64_t rect_bypass; uint64_t rect_partial; + /* NCLIPs while geometry correction is armed (docs/ENHANCEMENTS.md G1.12): + * nclip_precise had an exact determinant; nclip_disagree: its sign (non- + * zero) differs from the integer MAC0's, so the game's cull decision on + * that face disagrees with the face PGXP draws -- the crack exposure that + * tri_* cannot see, because a culled face draws no triangle at all; + * nclip_corrected: of those, MAC0 replaced by precise culling. */ + uint64_t nclip_precise; + uint64_t nclip_disagree; + uint64_t nclip_corrected; } PGXPStats; void pgxp_get_stats(PGXPStats *out); @@ -216,6 +252,9 @@ void pgxp_note_triangle(int precise); /* One rectangle-shortcut quad sent down the triangle path (all_precise) or * kept on the shortcut with only some precise vertices. */ void pgxp_note_rect_bypass(int all_precise); +/* One NCLIP whose exact sign disagreed with MAC0, and whether MAC0 was + * corrected. */ +void pgxp_note_nclip(int disagree, int corrected); /* --- gte.cpp forwarding surface (v14 ABI compat) -------------------------- */ diff --git a/runtime/include/pgxp_session.h b/runtime/include/pgxp_session.h index ea9138dc..7d72f6b7 100644 --- a/runtime/include/pgxp_session.h +++ b/runtime/include/pgxp_session.h @@ -2,25 +2,43 @@ #define PSXRECOMP_PGXP_SESSION_H /* - * Which PGXP corrections a session arms. main.cpp's renderer setup calls this - * once per session (the first boot and every rematch), after mod activation; - * it lives here so runtime/tests/test_pgxp_session.c drives the same code. + * Which PGXP corrections a session arms (docs/ENHANCEMENTS.md G1.11/G1.12). + * + * main.cpp drives the whole lifecycle through the two functions at the end of + * this header, compiled from src/pgxp_session.cpp, which + * runtime/tests/test_pgxp_session.cpp links and drives the same way: + * + * reset_mod_owned_presentation() -> psx_pgxp_session_reset() + * mod_runtime_activate_plugins() -> psx.enhancement.pgxp's activation + * records pgxp_mod_request(...) + * renderer setup (session_reboot) -> psx_pgxp_session_arm(&config, ...) + * + * The arm TAKES the mod request (reads and clears it), so a request lives only + * from one session's activation to that session's arming. A later session + * with an empty plan -- netplay clears it, or the player disabled the mod -- + * cannot inherit it, even if the reset were skipped. * * Inputs, in order of precedence: * 1. env_*: the validation overrides PSX_GEOMETRY_CORRECTION, - * PSX_PERSPECTIVE_TEXTURING and PSX_PGXP_CPU_MODE (-1 when unset, else - * 0 or 1). They win over everything, so an A/B run can switch PGXP off - * under an enabled mod. + * PSX_PERSPECTIVE_TEXTURING, PSX_PGXP_CPU_MODE and PSX_PGXP_CULLING (-1 + * when unset, else 0 or 1). They win over everything, so an A/B run can + * switch PGXP off under an enabled mod. * 2. video_* OR mod_*: the [video] baseline (game.toml, then settings.toml - * and the launcher) and the psx.enhancement.pgxp request recorded by - * this session's activation (pgxp_mod_requested()). Either one arms a - * correction. A session whose plan is empty -- netplay clears it, or the - * player disabled the mod -- has no request and gets the baseline alone. + * and the launcher) and the psx.enhancement.pgxp request this session's + * activation recorded. Either one arms a correction. With mod_only + * ([video] pgxp_mod_only, a title that ships PGXP through the mod) the + * baseline is ignored and the mod is the one switch. * * The mod arms both geometry and texture correction; its CPU-mode option adds - * tier-2 propagation. + * tier-2 propagation and its culling option precise culling. Precise culling + * changes guest-visible NCLIP results, so it has no [video] key (netplay does + * not clear [video]); it needs geometry correction. */ +#ifdef __cplusplus +extern "C" { +#endif + typedef struct PSXPgxpSessionInputs { int video_geometry; int video_texture; @@ -28,14 +46,18 @@ typedef struct PSXPgxpSessionInputs { int env_geometry; int env_texture; int env_cpu_mode; + int env_culling; int mod_enabled; int mod_cpu_mode; + int mod_culling; + int mod_only; } PSXPgxpSessionInputs; typedef struct PSXPgxpSessionArm { int geometry; int texture; int cpu_mode; + int culling; } PSXPgxpSessionArm; static inline int psx_pgxp_session_pick(int env, int baseline, int mod) { @@ -46,12 +68,19 @@ static inline int psx_pgxp_session_pick(int env, int baseline, int mod) { static inline PSXPgxpSessionArm psx_pgxp_session_resolve( const PSXPgxpSessionInputs* in) { PSXPgxpSessionArm arm; - arm.geometry = psx_pgxp_session_pick(in->env_geometry, in->video_geometry, + const int use_video = in->mod_only ? 0 : 1; + arm.geometry = psx_pgxp_session_pick(in->env_geometry, + use_video && in->video_geometry, in->mod_enabled); - arm.texture = psx_pgxp_session_pick(in->env_texture, in->video_texture, + arm.texture = psx_pgxp_session_pick(in->env_texture, + use_video && in->video_texture, in->mod_enabled); - arm.cpu_mode = psx_pgxp_session_pick(in->env_cpu_mode, in->video_cpu_mode, + arm.cpu_mode = psx_pgxp_session_pick(in->env_cpu_mode, + use_video && in->video_cpu_mode, in->mod_enabled && in->mod_cpu_mode); + arm.culling = arm.geometry && + psx_pgxp_session_pick(in->env_culling, 0, + in->mod_enabled && in->mod_culling); return arm; } @@ -62,4 +91,32 @@ static inline int psx_pgxp_session_env_flag(const char* value) { return (*value && *value != '0') ? 1 : 0; } +/* The [video] PGXP settings main.cpp hands the arming: the player's baseline + * (game.toml, then settings.toml and the launcher) and the title's tuning + * keys. */ +typedef struct PSXPgxpSessionConfig { + int video_geometry; + int video_texture; + int video_cpu_mode; + float tolerance; + int position_fallback; + int preserve_projection; + int mod_only; +} PSXPgxpSessionConfig; + +/* Session start, before plugin activation: forget any mod request. */ +void psx_pgxp_session_reset(void); + +/* The renderer setup's arming, once per session after activation: reads the + * env overrides, takes the mod request, resolves, and applies the result to + * the engine (gte_geometry_correction_set, gpu_texture_correction_set, + * pgxp_set_cpu_mode / _culling) along with the tuning keys. `used` (may be + * NULL) receives the inputs it resolved from. */ +PSXPgxpSessionArm psx_pgxp_session_arm(const PSXPgxpSessionConfig* config, + PSXPgxpSessionInputs* used); + +#ifdef __cplusplus +} +#endif + #endif diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index bcb6df95..0c3b75b4 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -388,6 +388,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/freeze_heartbeat.c ${PSXRECOMP_ROOT}/runtime/src/gte.cpp ${PSXRECOMP_ROOT}/runtime/src/pgxp.cpp + ${PSXRECOMP_ROOT}/runtime/src/pgxp_session.cpp ${PSXRECOMP_ROOT}/runtime/src/nd_intro_ot.c ${PSXRECOMP_ROOT}/runtime/src/crc32.c ${PSXRECOMP_ROOT}/runtime/src/psx_sha256.c diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 5a3abb90..3928db39 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -5276,7 +5276,7 @@ static void handle_geom_correction(int id, const char *json) uint64_t tc_att = 0, tc_arm = 0, tc_off = 0, tc_nosrc = 0, tc_noz = 0; gpu_texture_correction_stats(&tc_att, &tc_arm, &tc_off, &tc_nosrc, &tc_noz); send_fmt("{\"id\":%d,\"ok\":true," - "\"geometry_correction\":%d," + "\"geometry_correction\":%d,\"texture_correction\":%d," "\"geometry_vertex_hits\":%u," "\"perspective_triangles\":%u," "\"texcorr\":{\"attempts\":%llu,\"armed\":%llu," @@ -5284,15 +5284,19 @@ static void handle_geom_correction(int id, const char *json) "\"lookups\":%u,\"miss_unrecorded\":%u,\"miss_ambiguous\":%u," "\"pgxp\":{\"enabled\":%d,\"cpu_mode\":%d,\"tolerance\":%.3f," "\"position_fallback\":%d,\"preserve_projection\":%d," + "\"culling\":%d," "\"lookups\":%llu,\"dataflow_hit\":%llu,\"fallback_hit\":%llu," "\"native\":%llu,\"value_mismatch\":%llu,\"trunc_reject\":%llu," "\"tolerance_reject\":%llu,\"w_valid\":%llu," "\"produced\":%llu,\"swc2_stores\":%llu,\"ppp_produced\":%llu," "\"ppp_window_fallback\":%llu," "\"tri_precise\":%llu,\"tri_mixed\":%llu,\"tri_native\":%llu," - "\"rect_bypass\":%llu,\"rect_partial\":%llu}}", + "\"rect_bypass\":%llu,\"rect_partial\":%llu," + "\"nclip_precise\":%llu,\"nclip_disagree\":%llu," + "\"nclip_corrected\":%llu}}", id, gte_geometry_correction_enabled(), + gpu_texture_correction_enabled(), (unsigned)hits, (unsigned)gpu_texture_correction_hits(), (unsigned long long)tc_att, (unsigned long long)tc_arm, @@ -5301,6 +5305,7 @@ static void handle_geom_correction(int id, const char *json) (unsigned)lookups, (unsigned)unrec, (unsigned)ambig, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(), pgxp_position_fallback(), pgxp_preserve_projection(), + pgxp_culling(), (unsigned long long)ps.lookups, (unsigned long long)ps.dataflow_hit, (unsigned long long)ps.fallback_hit, @@ -5317,14 +5322,17 @@ static void handle_geom_correction(int id, const char *json) (unsigned long long)ps.tri_mixed, (unsigned long long)ps.tri_native, (unsigned long long)ps.rect_bypass, - (unsigned long long)ps.rect_partial); + (unsigned long long)ps.rect_partial, + (unsigned long long)ps.nclip_precise, + (unsigned long long)ps.nclip_disagree, + (unsigned long long)ps.nclip_corrected); } /* pgxp — live-tune the value-propagation engine for one-toggle isolation runs * without a rebuild: {"cmd":"pgxp","cpu_mode":0|1,"tolerance":F, - * "position_fallback":0|1,"preserve_projection":0|1}. Fields are optional; the - * reply echoes the resulting state (same shape as geom_correction's "pgxp" - * object, flattened). */ + * "position_fallback":0|1,"preserve_projection":0|1,"culling":0|1}. Fields + * are optional; the reply echoes the resulting state (same shape as + * geom_correction's "pgxp" object, flattened). */ static void handle_pgxp(int id, const char *json) { /* Live toggles for the one-toggle-at-a-time A/B protocol (docs/ENHANCEMENTS.md @@ -5348,6 +5356,9 @@ static void handle_pgxp(int id, const char *json) int pp = json_get_int(json, "preserve_projection", -1); if (pp >= 0) pgxp_set_preserve_projection(pp != 0); + int cu = json_get_int(json, "culling", -1); + if (cu >= 0) + pgxp_set_culling(cu != 0); /* tolerance is fractional (sub-pixel), so scan it directly — json_get_int * would truncate 0.5 to 0. */ const char *p = strstr(json, "\"tolerance\""); @@ -5359,9 +5370,11 @@ static void handle_pgxp(int id, const char *json) } send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"cpu_mode\":%d," "\"tolerance\":%.3f,\"position_fallback\":%d," - "\"preserve_projection\":%d,\"suppress\":%u,\"active\":%d}", + "\"preserve_projection\":%d,\"culling\":%d," + "\"suppress\":%u,\"active\":%d}", id, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(), pgxp_position_fallback(), pgxp_preserve_projection(), + pgxp_culling(), (unsigned)pgxp_test_suppress_depth(), pgxp_test_active()); } diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index b9c642a2..81067843 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -9,6 +9,9 @@ extern "C" uint32_t psx_read_word(uint32_t addr); extern "C" int gpu_ws_precise_nclip_enabled(void); +/* overlay_loader.c: nonzero while an overlay shadow diff (interpreter record + * plus native replay of one candidate) is running. */ +extern "C" int psx_overlay_shadow_diff_active(void); namespace PSXRecomp { namespace GTE { @@ -992,6 +995,37 @@ void gte_nclip(GTEState* gte, uint32_t instr) { (int64_t)sx2 * (sy0 - sy1); gte->check_mac0_overflow(mac0); int32_t out = static_cast(mac0); + /* PGXP's NCLIP (docs/ENHANCEMENTS.md G1.12). While geometry correction is + * armed, compare the sign of the exact determinant of the three vertices + * PGXP will draw with the integer one; a disagreement is a face the game + * culls (or keeps) on a winding the drawn face does not have. Counted + * always; with precise culling on, MAC0 takes the exact sign. Never in a + * pass that is compared against another execution of the same code: the + * speculative and replay passes record no shadows, so the overlay shadow + * diff holds it off for its interpreter pass too and both passes see the + * integer MAC0. FLAG keeps the integer result's overflow bits. */ + if (s_geom_enabled && !s_gte_replay_sandbox && s_speculative_depth == 0 && + !psx_overlay_shadow_diff_active()) { + const uint32_t words[3] = { (uint32_t)gte->SXY[0], (uint32_t)gte->SXY[1], + (uint32_t)gte->SXY[2] }; + int64_t cross = 0; + if (pgxp_gte_nclip_precise(words, &cross)) { + const int sp = (cross > 0) - (cross < 0); + const int sn = (out > 0) - (out < 0); + if (sp != 0 && sp != sn) { + const int corrected = pgxp_culling(); + if (corrected) { + const uint64_t mag = cross < 0 ? (uint64_t)(-cross) + : (uint64_t)cross; + uint64_t area = (mag + (1ull << 31)) >> 32; + if (area < 1) area = 1; + if (area > 0x7FFFFFFFull) area = 0x7FFFFFFFull; + out = sp > 0 ? (int32_t)area : -(int32_t)area; + } + pgxp_note_nclip(1, corrected); + } + } + } s_nclip_last_native = out; s_nclip_last_precise_valid = false; /* Compute an exact 16.16 determinant for configured branch consumers, but diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index cdb1ccec..a289498b 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1237,6 +1237,11 @@ static float g_video_pgxp_tolerance = 0.5f; * Defaults keep the historical behaviour. game.toml [video] only. */ static int g_video_pgxp_position_fallback = 1; static int g_video_pgxp_preserve_projection = 0; +/* [video] pgxp_mod_only (G1.12): the title ships PGXP through the + * psx.enhancement.pgxp mod, which is then the one switch -- the [video] + * geometry_correction / perspective_texturing / pgxp_cpu_mode values are not + * applied and the launcher hides its Perspective textures row. */ +static int g_video_pgxp_mod_only = 0; static int g_video_renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER; /* Settings -> Display -> Internal resolution (internal_resolution.h). The @@ -1555,8 +1560,9 @@ static void reset_mod_owned_presentation(void) { g_frame_interpolation_blend = g_frame_interpolation_blend_default; g_frame_interpolation_source = PSX_MOD_FRAME_SOURCE_VBLANK; /* The PGXP mod's request: only this session's activation may set it; the - * renderer setup combines it with the [video] baseline (pgxp_session.h). */ - pgxp_mod_request(0, 0); + * renderer setup takes it and combines it with the [video] baseline + * (pgxp_session.h). */ + psx_pgxp_session_reset(); /* Render-pass counters, the disabled-after-faults latch and any open * plan generation belong to the session that made them. */ render_pass_reset_session(); @@ -12886,7 +12892,12 @@ namespace { gi->assist_binding_count = PSX_ASSIST_BIND_COUNT; gi->has_skip_fmv = skip_fmv_offered_b ? 1 : 0; gi->has_turbo_loads = turbo_loads_offered_b ? 1 : 0; - gi->has_geometry_precision = 1; + /* The Perspective textures row. Hidden for a title that ships PGXP + * through the psx.enhancement.pgxp mod ([video] pgxp_mod_only): the + * Mods page is then its one switch, and the row's value is not + * applied, so showing it would be a second control that does + * nothing. */ + gi->has_geometry_precision = g_video_pgxp_mod_only ? 0 : 1; gi->has_rewind_depth = 1; #if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) /* The Supersampling row becomes an Internal resolution list. */ @@ -13408,6 +13419,7 @@ int main(int argc, char** argv) { gc.runtime.video_pgxp_position_fallback ? 1 : 0; g_video_pgxp_preserve_projection = gc.runtime.video_pgxp_preserve_projection ? 1 : 0; + g_video_pgxp_mod_only = gc.runtime.video_pgxp_mod_only ? 1 : 0; g_video_renderer = gc.runtime.video_renderer; g_video_screen = gc.runtime.video_screen_kind; g_video_scanlines = gc.runtime.video_scanlines; @@ -15560,32 +15572,26 @@ session_reboot: * measured from the first projected vertex — a TCP toggle always arrives * after the interesting window. '0' = off, anything else = on. They win * over the PGXP mod too, so an A/B run can switch it off. */ + /* The [video] baseline plus the psx.enhancement.pgxp request this + * session's activation recorded (start_mod_session ran above, for the + * first boot and for a rematch); psx_pgxp_session_arm takes that request, + * so it cannot outlive this session. Applying the baseline alone here + * used to switch an enabled mod straight back off. The g_video_* values + * stay the player's settings: the mod never leaks into a launcher seed. + * With [video] pgxp_mod_only the mod is the one switch (pgxp_session.h). */ + PSXPgxpSessionConfig pgxp_cfg{}; + pgxp_cfg.video_geometry = g_video_geometry_correction; + pgxp_cfg.video_texture = g_video_perspective_texturing; + pgxp_cfg.video_cpu_mode = g_video_pgxp_cpu_mode; + pgxp_cfg.tolerance = g_video_pgxp_tolerance; + pgxp_cfg.position_fallback = g_video_pgxp_position_fallback; + pgxp_cfg.preserve_projection = g_video_pgxp_preserve_projection; + pgxp_cfg.mod_only = g_video_pgxp_mod_only; PSXPgxpSessionInputs pgxp_in{}; - pgxp_in.env_geometry = - psx_pgxp_session_env_flag(std::getenv("PSX_GEOMETRY_CORRECTION")); - pgxp_in.env_texture = - psx_pgxp_session_env_flag(std::getenv("PSX_PERSPECTIVE_TEXTURING")); - pgxp_in.env_cpu_mode = - psx_pgxp_session_env_flag(std::getenv("PSX_PGXP_CPU_MODE")); + const PSXPgxpSessionArm pgxp_arm = psx_pgxp_session_arm(&pgxp_cfg, &pgxp_in); if (pgxp_in.env_geometry >= 0) g_video_geometry_correction = pgxp_in.env_geometry; if (pgxp_in.env_texture >= 0) g_video_perspective_texturing = pgxp_in.env_texture; if (pgxp_in.env_cpu_mode >= 0) g_video_pgxp_cpu_mode = pgxp_in.env_cpu_mode; - /* The [video] baseline plus the psx.enhancement.pgxp request this - * session's activation recorded (start_mod_session ran above, for the - * first boot and for a rematch). Applying the baseline alone here used to - * switch an enabled mod straight back off. The g_video_* values stay the - * player's settings: the mod never leaks into a launcher seed. */ - pgxp_in.video_geometry = g_video_geometry_correction; - pgxp_in.video_texture = g_video_perspective_texturing; - pgxp_in.video_cpu_mode = g_video_pgxp_cpu_mode; - pgxp_in.mod_enabled = pgxp_mod_requested(&pgxp_in.mod_cpu_mode); - const PSXPgxpSessionArm pgxp_arm = psx_pgxp_session_resolve(&pgxp_in); - gte_geometry_correction_set(pgxp_arm.geometry); - gpu_texture_correction_set(pgxp_arm.texture); - pgxp_set_cpu_mode(pgxp_arm.cpu_mode); - pgxp_set_tolerance(g_video_pgxp_tolerance); - pgxp_set_position_fallback(g_video_pgxp_position_fallback); - pgxp_set_preserve_projection(g_video_pgxp_preserve_projection); /* Scanlines: env override wins over config, same as the corrections above, * so a headless/free-run boot can be captured with the effect armed from the * first present. PSX_SCANLINES=0/1; PSX_SCANLINE_STRENGTH=0..1. Pushed to the @@ -15600,9 +15606,10 @@ session_reboot: g_video_scanline_strength); if (pgxp_arm.geometry || pgxp_arm.texture) { std::fprintf(stdout, - "psxrecomp: geometry correction %s, perspective texturing %s%s%s\n", + "psxrecomp: geometry correction %s, perspective texturing %s%s%s%s\n", pgxp_arm.geometry ? "on" : "off", pgxp_arm.texture ? "on" : "off", + pgxp_arm.culling ? ", precise culling" : "", pgxp_in.mod_enabled ? " (PGXP mod)" : "", (pgxp_arm.geometry && requested_scale < 2) ? " (needs [video] supersampling >= 2 to be visible)" : ""); diff --git a/runtime/src/mod_builtin_pgxp.c b/runtime/src/mod_builtin_pgxp.c index 85ac05f9..cc7baf1c 100644 --- a/runtime/src/mod_builtin_pgxp.c +++ b/runtime/src/mod_builtin_pgxp.c @@ -5,14 +5,15 @@ * the emulated GTE/GPU pair, not of any particular disc, so it ships here and * mods/builtin/packages/psx.enhancement.pgxp targets game_id "*". Default off * (the faithful floor, docs/ENHANCEMENTS.md G1.9); enabling arms geometry and - * texture correction on the value-propagation engine (pgxp.cpp), cpu-mode per - * the mod option. The title's other [video] PGXP keys (the tolerance clamp, - * default 0.5px) still apply. + * texture correction on the value-propagation engine (pgxp.cpp), cpu-mode and + * precise culling (G1.12) per the mod options. The title's other [video] PGXP + * keys (the tolerance clamp, default 0.5px) still apply. * * Activation only RECORDS the request (pgxp_mod_request). It runs before the - * renderer setup in main.cpp, which applies the [video] baseline and arms the - * engine from both (pgxp_session.h). Arming here directly used to be undone - * by that baseline, so the mod did nothing (docs/ENHANCEMENTS.md G1.11). + * renderer setup in main.cpp, whose session arming (pgxp_session.cpp) takes + * the request and arms the engine from it and the [video] baseline. Arming + * here directly used to be undone by that baseline, so the mod did nothing + * (docs/ENHANCEMENTS.md G1.11). * * Coverage note: the engine reaches near-total dataflow coverage on a binary * compiled with the PGXP hook variant (-DPSX_PGXP=1, PSX_PGXP_VARIANT); on a @@ -34,7 +35,8 @@ static int pgxp_option_flag(const char* feature, const char* id) { } static void builtin_pgxp_activate(void) { - pgxp_mod_request(1, pgxp_option_flag("pgxp", "cpu_mode")); + pgxp_mod_request(1, pgxp_option_flag("pgxp", "cpu_mode"), + pgxp_option_flag("pgxp", "culling")); } PSX_MOD_CONSTRUCTOR(psx_register_builtin_pgxp_plugin) { diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 4edd8c0d..deccd272 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -362,6 +362,11 @@ static int s_diff_mode = 0; * entry while every other validated candidate follows its normal live route. */ static uint32_t s_diff_addr = 0; static int s_in_shadow = 0; +/* gte.cpp holds PGXP's precise culling off while a shadow diff runs: it reads + * host-only shadows, which the native (speculative) pass does not record, so + * the two passes would see different NCLIP results (docs/ENHANCEMENTS.md + * G1.12). */ +int psx_overlay_shadow_diff_active(void) { return s_in_shadow; } /* Candidate whose shadow NATIVE pass is currently executing (NULL outside it). * Set ONLY around run_shadow_diff's native pass — never during the interp pass, * which must stay pure interp. Lets the CPS continuation re-entry path below diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index 8a1d5f84..29503fa3 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -81,6 +81,7 @@ static int s_cpu_mode = 0; static float s_tolerance = 0.5f; /* user-validated seam clamp (G1.10) */ static int s_position_fallback = 1; /* G1.4 cache tier (G1.11 switch) */ static int s_preserve_projection = 0; /* exact-projection shadows (G1.11) */ +static int s_culling = 0; /* precise NCLIP sign (G1.12) */ static uint32_t s_suppress = 0; static int s_deferred_invalidate = 0; @@ -144,19 +145,31 @@ extern "C" void pgxp_set_preserve_projection(int enabled) { } extern "C" int pgxp_preserve_projection(void) { return s_preserve_projection; } +extern "C" void pgxp_set_culling(int enabled) { s_culling = enabled ? 1 : 0; } +extern "C" int pgxp_culling(void) { return s_culling; } + static int s_mod_request = 0; static int s_mod_request_cpu = 0; +static int s_mod_request_cull = 0; -extern "C" void pgxp_mod_request(int enabled, int cpu_mode) { +extern "C" void pgxp_mod_request(int enabled, int cpu_mode, int culling) { s_mod_request = enabled ? 1 : 0; s_mod_request_cpu = (enabled && cpu_mode) ? 1 : 0; + s_mod_request_cull = (enabled && culling) ? 1 : 0; } -extern "C" int pgxp_mod_requested(int *cpu_mode) { +extern "C" int pgxp_mod_requested(int *cpu_mode, int *culling) { if (cpu_mode) *cpu_mode = s_mod_request_cpu; + if (culling) *culling = s_mod_request_cull; return s_mod_request; } +extern "C" int pgxp_mod_request_take(int *cpu_mode, int *culling) { + const int requested = pgxp_mod_requested(cpu_mode, culling); + pgxp_mod_request(0, 0, 0); + return requested; +} + extern "C" void pgxp_suppress_begin(void) { ++s_suppress; recompute_active(); @@ -181,6 +194,11 @@ extern "C" void pgxp_note_rect_bypass(int all_precise) { else s_stats.rect_partial++; } +extern "C" void pgxp_note_nclip(int disagree, int corrected) { + if (disagree) s_stats.nclip_disagree++; + if (corrected) s_stats.nclip_corrected++; +} + extern "C" void pgxp_note_triangle(int precise) { if (precise >= 3) s_stats.tri_precise++; else if (precise > 0) s_stats.tri_mixed++; @@ -757,6 +775,35 @@ extern "C" int pgxp_get_gte_sxy_checked(uint32_t index, uint32_t expect, return 1; } +/* Defined below with the GPU consumer. */ +static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x, + int32_t int_y, uint32_t word); + +extern "C" int pgxp_gte_nclip_precise(const uint32_t sxy[3], int64_t *cross) { + if (!g_pgxp_active) return 0; + int64_t x[3], y[3]; + for (int i = 0; i < 3; i++) { + const PGXPValue *pv = &s_gte[12 + i]; + if (!pv_live(pv) || (pv->flags & PGXP_F_VXY) != PGXP_F_VXY || + pv->value != sxy[i]) + return 0; + /* The GPU parses the packet half as the same signed integer for every + * value the GTE can produce (its saturation limits are -0x400 and + * 0x3FF), so the register half stands in for the native parse. */ + if (pgxp_accept(pv->x16, pv->y16, (int16_t)(sxy[i] & 0xFFFFu), + (int16_t)(sxy[i] >> 16), sxy[i]) != 0) + return 0; + x[i] = pv->x16; + y[i] = pv->y16; + } + /* NCLIP's determinant, sx0*(sy1-sy2) + sx1*(sy2-sy0) + sx2*(sy0-sy1), + * on 16.16 positions. Positions are bounded to +-2^28 (the transport + * clamp), so every product fits in 64 bits. */ + *cross = (x[1] - x[0]) * (y[2] - y[0]) - (y[1] - y[0]) * (x[2] - x[0]); + s_stats.nclip_precise++; + return 1; +} + extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) { /* Invalidation-class bookkeeping: runs even with the engine disarmed so * seeded/leftover shadows can never outlive a guest register write. Only @@ -787,6 +834,25 @@ static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half) { d < (int64_t)PGXP_PPP_AGREE_ABOVE * 65536; } +/* The consumer-side safeguards on one candidate position: truncation + * agreement on both axes, then the tolerance clamp. Returns 0 when accepted, + * 1 for a truncation reject, 2 for a tolerance reject. Shared by the GPU + * lookup and the precise NCLIP so culling and drawing believe exactly the + * same vertices. */ +static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x, + int32_t int_y, uint32_t word) { + if (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu)) || + !pgxp_agrees(py, int_y, (int16_t)(word >> 16))) + return 1; + if (s_tolerance >= 0.0f) { + float dx = (float)((int64_t)px - (int64_t)int_x * 65536) * (1.0f / 65536.0f); + float dy = (float)((int64_t)py - (int64_t)int_y * 65536) * (1.0f / 65536.0f); + if (std::fabs(dx) > s_tolerance || std::fabs(dy) > s_tolerance) + return 2; + } + return 0; +} + extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word, int32_t int_x, int32_t int_y, int32_t *x16, int32_t *y16, @@ -821,18 +887,15 @@ extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word, if (have) { /* Truncation agreement: the GPU parsed 11-bit integers out of the * packet; a precise position whose integer part disagrees (a - * wrapped/CPU-modified coordinate) must not be believed. */ - if (!pgxp_agrees(px, int_x, (int16_t)(packet_word & 0xFFFFu)) || - !pgxp_agrees(py, int_y, (int16_t)(packet_word >> 16))) { + * wrapped/CPU-modified coordinate) must not be believed. Then the + * tolerance clamp. */ + const int why = pgxp_accept(px, py, int_x, int_y, packet_word); + if (why == 1) { s_stats.trunc_reject++; have = 0; - } else if (s_tolerance >= 0.0f) { - float dx = (float)((int64_t)px - (int64_t)int_x * 65536) * (1.0f / 65536.0f); - float dy = (float)((int64_t)py - (int64_t)int_y * 65536) * (1.0f / 65536.0f); - if (std::fabs(dx) > s_tolerance || std::fabs(dy) > s_tolerance) { - s_stats.tolerance_reject++; - have = 0; - } + } else if (why == 2) { + s_stats.tolerance_reject++; + have = 0; } } diff --git a/runtime/src/pgxp_session.cpp b/runtime/src/pgxp_session.cpp new file mode 100644 index 00000000..eed7ac0d --- /dev/null +++ b/runtime/src/pgxp_session.cpp @@ -0,0 +1,45 @@ +/* pgxp_session.cpp -- the per-session PGXP arming main.cpp runs + * (docs/ENHANCEMENTS.md G1.11/G1.12). See pgxp_session.h for the sequence. + * Kept out of main.cpp so runtime/tests/test_pgxp_session.cpp runs exactly + * this code against the real mod runtime and builtin plugin. */ +#include "pgxp_session.h" +#include "pgxp.h" + +#include + +extern "C" void gte_geometry_correction_set(int enabled); +extern "C" void gpu_texture_correction_set(int enabled); + +extern "C" void psx_pgxp_session_reset(void) { + pgxp_mod_request(0, 0, 0); +} + +extern "C" PSXPgxpSessionArm psx_pgxp_session_arm( + const PSXPgxpSessionConfig* config, PSXPgxpSessionInputs* used) { + PSXPgxpSessionInputs in{}; + in.env_geometry = + psx_pgxp_session_env_flag(std::getenv("PSX_GEOMETRY_CORRECTION")); + in.env_texture = + psx_pgxp_session_env_flag(std::getenv("PSX_PERSPECTIVE_TEXTURING")); + in.env_cpu_mode = + psx_pgxp_session_env_flag(std::getenv("PSX_PGXP_CPU_MODE")); + in.env_culling = + psx_pgxp_session_env_flag(std::getenv("PSX_PGXP_CULLING")); + in.video_geometry = config->video_geometry ? 1 : 0; + in.video_texture = config->video_texture ? 1 : 0; + in.video_cpu_mode = config->video_cpu_mode ? 1 : 0; + in.mod_only = config->mod_only ? 1 : 0; + /* Take, not peek: the request belongs to this session alone. */ + in.mod_enabled = pgxp_mod_request_take(&in.mod_cpu_mode, &in.mod_culling); + + const PSXPgxpSessionArm arm = psx_pgxp_session_resolve(&in); + pgxp_set_tolerance(config->tolerance); + pgxp_set_position_fallback(config->position_fallback); + pgxp_set_preserve_projection(config->preserve_projection); + pgxp_set_culling(arm.culling); + gte_geometry_correction_set(arm.geometry); + gpu_texture_correction_set(arm.texture); + pgxp_set_cpu_mode(arm.cpu_mode); + if (used) *used = in; + return arm; +} diff --git a/runtime/tests/test_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index e65a8207..de4a1e2b 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -51,6 +51,8 @@ extern "C" int gpu_ws_precise_nclip_enabled(void) { return g_test_precise_nclip_enabled; } extern "C" void gpu_pgxp_rederive_enable(void) {} +static int g_test_shadow_diff = 0; +extern "C" int psx_overlay_shadow_diff_active(void) { return g_test_shadow_diff; } extern "C" void psx_ws_note_gte_project(int) {} extern "C" { uint64_t s_frame_count = 0; @@ -627,6 +629,144 @@ int test_precise_nclip_is_title_scoped() { return 0; } +/* PGXP precise culling (docs/ENHANCEMENTS.md G1.12): NCLIP's MAC0 takes the + * exact sign only while geometry correction and culling are armed, only when + * the three SXY shadows are believed, and never in a compared pass. */ +int test_pgxp_culling() { + gte_precision_tracking_set(1); + gte_geometry_correction_set(1); + pgxp_set_preserve_projection(1); + pgxp_set_tolerance(-1.0f); + + /* A far road row: the three vertices all round to y = 113 (MAC0 0, the + * game culls it); exact y 112.64 / 113.12 / 113.83 (positive area). */ + const uint32_t packed[3] = {(113u << 16) | 100u, (113u << 16) | 220u, + (113u << 16) | 160u}; + const int32_t x16[3] = {100 << 16, 220 << 16, 160 << 16}; + const int32_t y16[3] = {(112 << 16) + 41943, (113 << 16) + 7864, + (113 << 16) + 54394}; + const int64_t cross = + ((int64_t)x16[1] - x16[0]) * ((int64_t)y16[2] - y16[0]) - + ((int64_t)y16[1] - y16[0]) * ((int64_t)x16[2] - x16[0]); + const int32_t area = (int32_t)((uint64_t)(cross + (1ll << 31)) >> 32); + auto nclip = [&](const uint32_t (&w)[3], const int32_t (&xs)[3], + const int32_t (&ys)[3], uint32_t *flag) { + CPUState cpu{}; + for (uint32_t i = 0; i < 3; ++i) { + cpu.gte_data[12 + i] = w[i]; + gte_test_seed_precise_projection(i, w[i], xs[i], ys[i], 100); + } + gte_execute(&cpu, 0x06u); + if (flag) *flag = cpu.gte_ctrl[31]; + return static_cast(cpu.gte_data[24]); + }; + PGXPStats a{}, b{}; + pgxp_get_stats(&a); + uint32_t flag_off = 0, flag_on = 0; + + /* Culling off: the guest sees the hardware's 0; the disagreement is + * counted (the crack exposure), not corrected. */ + pgxp_set_culling(0); + if (int32_t m = nclip(packed, x16, y16, &flag_off); m != 0) + return fail_value("culling off keeps MAC0", 0, 0x06u, 0, 0u, (uint32_t)m); + pgxp_get_stats(&b); + if (b.nclip_disagree != a.nclip_disagree + 1 || + b.nclip_corrected != a.nclip_corrected) + return fail_value("culling off counts the disagreement", 0, 0x06u, 0, + 1u, (uint32_t)(b.nclip_disagree - a.nclip_disagree)); + + /* Culling on: MAC0 is the exact doubled area, rounded (>= 1). */ + pgxp_set_culling(1); + if (area < 1) + return fail_value("test area", 0, 0x06u, 0, 1u, (uint32_t)area); + if (int32_t m = nclip(packed, x16, y16, &flag_on); m != area) + return fail_value("culling on takes the exact sign", 0, 0x06u, 0, + (uint32_t)area, (uint32_t)m); + if (flag_on != flag_off) + return fail_value("culling keeps FLAG", 0, 0x06u, 0, flag_off, flag_on); + pgxp_get_stats(&a); + if (a.nclip_corrected != b.nclip_corrected + 1) + return fail_value("culling on counts the correction", 0, 0x06u, 0, 1u, + (uint32_t)(a.nclip_corrected - b.nclip_corrected)); + + /* Reversed winding: the exact sign is negative, magnitude at least 1. */ + { + const uint32_t w[3] = {packed[0], packed[2], packed[1]}; + const int32_t xs[3] = {x16[0], x16[2], x16[1]}; + const int32_t ys[3] = {y16[0], y16[2], y16[1]}; + if (int32_t m = nclip(w, xs, ys, nullptr); m != -area) + return fail_value("culling keeps the reversed sign", 0, 0x06u, 0, + (uint32_t)-area, (uint32_t)m); + } + /* A native-positive sliver whose exact winding is reversed: the game + * now sees it back-facing. Native (0,0),(10,0),(5,1): MAC0 +10; exact + * y2 = -0.25 (inside the window). */ + { + const uint32_t w[3] = {0u, 10u, (1u << 16) | 5u}; + const int32_t xs[3] = {0, 10 << 16, 5 << 16}; + const int32_t ys[3] = {0, 0, -(1 << 14)}; + pgxp_set_culling(0); + if (int32_t m = nclip(w, xs, ys, nullptr); m != 10) + return fail_value("native sliver MAC0", 0, 0x06u, 0, 10u, (uint32_t)m); + pgxp_set_culling(1); + if (int32_t m = nclip(w, xs, ys, nullptr); m != -3) + return fail_value("reversed sliver MAC0", 0, 0x06u, 0, + (uint32_t)-3, (uint32_t)m); + } + /* Signs that agree keep the integer MAC0 exactly. */ + { + const uint32_t w[3] = {0u, 10u, (4u << 16) | 5u}; + const int32_t xs[3] = {0, 10 << 16, 5 << 16}; + const int32_t ys[3] = {1 << 15, 0, (4 << 16) + (1 << 15)}; + if (int32_t m = nclip(w, xs, ys, nullptr); m != 40) + return fail_value("agreeing sign keeps MAC0", 0, 0x06u, 0, 40u, + (uint32_t)m); + } + /* A stale shadow (the register word changed under it) fails closed. */ + { + const uint32_t w[3] = {packed[0], packed[1], packed[2]}; + CPUState cpu{}; + for (uint32_t i = 0; i < 3; ++i) { + cpu.gte_data[12 + i] = w[i]; + gte_test_seed_precise_projection(i, w[i] ^ (i == 2 ? 1u : 0u), + x16[i], y16[i], 100); + } + gte_execute(&cpu, 0x06u); + if (cpu.gte_data[24] != 0u) + return fail_value("stale shadow keeps MAC0", 0, 0x06u, 0, 0u, + cpu.gte_data[24]); + } + /* Compared passes see the hardware result: the overlay shadow diff (its + * interpreter pass included) and a speculative pass. */ + g_test_shadow_diff = 1; + if (int32_t m = nclip(packed, x16, y16, nullptr); m != 0) + return fail_value("shadow diff holds culling off", 0, 0x06u, 0, 0u, + (uint32_t)m); + g_test_shadow_diff = 0; + gte_precision_speculative_begin(); + { + CPUState cpu{}; + for (uint32_t i = 0; i < 3; ++i) cpu.gte_data[12 + i] = packed[i]; + gte_execute(&cpu, 0x06u); + gte_precision_speculative_end(); + if (cpu.gte_data[24] != 0u) + return fail_value("speculative pass holds culling off", 0, 0x06u, + 0, 0u, cpu.gte_data[24]); + } + /* Geometry correction off: nothing is corrected or counted. */ + gte_geometry_correction_set(0); + pgxp_get_stats(&a); + if (int32_t m = nclip(packed, x16, y16, nullptr); m != 0) + return fail_value("geometry off keeps MAC0", 0, 0x06u, 0, 0u, (uint32_t)m); + pgxp_get_stats(&b); + if (b.nclip_precise != a.nclip_precise || b.nclip_disagree != a.nclip_disagree) + return fail_value("geometry off counts nothing", 0, 0x06u, 0, 0u, + (uint32_t)(b.nclip_precise - a.nclip_precise)); + pgxp_set_culling(0); + pgxp_set_preserve_projection(0); + return 0; +} + int test_precision_speculative_transaction() { constexpr uint32_t address = 0x00123450u; constexpr uint32_t packed = 0x00420021u; @@ -854,6 +994,7 @@ int main() { if (int rc = test_precise_nclip_is_title_scoped()) return rc; if (int rc = test_precision_speculative_transaction()) return rc; if (int rc = test_preserve_projection_is_shadow_only()) return rc; + if (int rc = test_pgxp_culling()) return rc; std::puts("PASS: canonical GTE register helpers match GTEState transfer oracle"); return 0; } diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp index 6b79bb9a..f77783a5 100644 --- a/runtime/tests/test_pgxp.cpp +++ b/runtime/tests/test_pgxp.cpp @@ -432,6 +432,75 @@ int main(void) { pgxp_set_preserve_projection(0); } + /* --- NCLIP's exact determinant (G1.12) -------------------------------- */ + { + /* A far road row: native y 113, 113, 113 (zero area), exact y + * 112.64 / 113.12 / 113.83 -> positive area. */ + const uint32_t w0 = (113u << 16) | 100u; + const uint32_t w1 = (113u << 16) | 220u; + const uint32_t w2 = (113u << 16) | 160u; + const int32_t y0 = (112 << 16) + (int32_t)(0.64 * 65536); + const int32_t y1 = (113 << 16) + (int32_t)(0.12 * 65536); + const int32_t y2 = (113 << 16) + (int32_t)(0.83 * 65536); + auto seed3 = [&](void) { + pgxp_test_seed_gte_sxy(0, w0, 100 << 16, y0, SZ3, 1); + pgxp_test_seed_gte_sxy(1, w1, 220 << 16, y1, SZ3, 1); + pgxp_test_seed_gte_sxy(2, w2, 160 << 16, y2, SZ3, 1); + }; + const uint32_t words[3] = { w0, w1, w2 }; + auto expect_cross = [&](void) { + return ((int64_t)(220 << 16) - (100 << 16)) * ((int64_t)y2 - y0) - + ((int64_t)y1 - y0) * ((int64_t)(160 << 16) - (100 << 16)); + }; + PGXPStats a, b; + pgxp_get_stats(&a); + int64_t cross = 0; + /* IR path: y0 = 112.64 does not truncate to 113 -> not believed */ + seed3(); + CHECK(pgxp_gte_nclip_precise(words, &cross) == 0); + /* preserve-projection window: believed, exact determinant */ + pgxp_set_preserve_projection(1); + seed3(); + CHECK(pgxp_gte_nclip_precise(words, &cross) == 1); + CHECK(cross == expect_cross()); + CHECK(cross > 0); + /* a stale shadow (word changed under it) fails closed */ + const uint32_t stale[3] = { w0, w1, w2 ^ 1u }; + CHECK(pgxp_gte_nclip_precise(stale, &cross) == 0); + /* the tolerance clamp applies as at the GPU */ + pgxp_set_tolerance(0.25f); + CHECK(pgxp_gte_nclip_precise(words, &cross) == 0); + pgxp_set_tolerance(-1.0f); + /* nothing inside a suppression bracket */ + pgxp_suppress_begin(); + CHECK(pgxp_gte_nclip_precise(words, &cross) == 0); + pgxp_suppress_end(); + pgxp_get_stats(&b); + CHECK(b.nclip_precise == a.nclip_precise + 1); + pgxp_note_nclip(1, 0); + pgxp_note_nclip(1, 1); + pgxp_get_stats(&b); + CHECK(b.nclip_disagree == a.nclip_disagree + 2); + CHECK(b.nclip_corrected == a.nclip_corrected + 1); + pgxp_set_preserve_projection(0); + } + + /* --- the mod request lives until the session's arming takes it -------- */ + { + CHECK(pgxp_culling() == 0); /* default off */ + pgxp_set_culling(1); + CHECK(pgxp_culling() == 1); + pgxp_set_culling(0); + int cpu = -1, cull = -1; + pgxp_mod_request(1, 1, 1); + CHECK(pgxp_mod_requested(&cpu, &cull) == 1 && cpu == 1 && cull == 1); + CHECK(pgxp_mod_requested(nullptr, nullptr) == 1); /* peek keeps it */ + CHECK(pgxp_mod_request_take(&cpu, &cull) == 1 && cpu == 1 && cull == 1); + CHECK(pgxp_mod_request_take(&cpu, &cull) == 0 && cpu == 0 && cull == 0); + pgxp_mod_request(0, 1, 1); /* options need the mod */ + CHECK(pgxp_mod_requested(&cpu, &cull) == 0 && cpu == 0 && cull == 0); + } + /* --- triangle census (G1.1 crack exposure) --- */ { PGXPStats a, b; diff --git a/runtime/tests/test_pgxp_session.cpp b/runtime/tests/test_pgxp_session.cpp index 2ee368b3..c1b81318 100644 --- a/runtime/tests/test_pgxp_session.cpp +++ b/runtime/tests/test_pgxp_session.cpp @@ -1,25 +1,49 @@ -/* test_pgxp_session.cpp -- does the psx.enhancement.pgxp mod arm PGXP? +/* 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 docs/ENHANCEMENTS.md 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 (pgxp_mod_request) and the renderer setup combines the two through - * psx_pgxp_session_resolve (pgxp_session.h). This test drives the real - * activation callback (src/mod_builtin_pgxp.c, registered through a stubbed - * mod API) and the real resolver, in the order main.cpp runs them: + * 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. * - * reset_mod_owned_presentation() -> pgxp_mod_request(0, 0) + * 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_resolve(...) + * 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 "mod_plugins.h" +#include "cpu_state.h" +#include "gpu.h" + +#include #include +#include #include +#include +#include +#include #include +#include + +#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) \ @@ -31,129 +55,256 @@ static int g_failures = 0; } \ } while (0) -/* ---- stubs: the mod API the builtin plugin calls ------------------------ */ - -static PSXModActivationCallback g_activation = nullptr; -static std::string g_activation_id; -static std::string g_cpu_mode_option = "false"; - -extern "C" int psx_mod_register_activation_plugin( - const char* id, PSXModActivationCallback callback) { - g_activation_id = id ? id : ""; - g_activation = callback; - return 1; -} - -extern "C" int psx_mod_option_value(const char* package_id, - const char* feature_id, - const char* option_id, char* out, - uint32_t out_size) { - if (std::strcmp(package_id, "psx.enhancement.pgxp") != 0 || - std::strcmp(feature_id, "pgxp") != 0 || - std::strcmp(option_id, "cpu_mode") != 0 || out_size == 0) - return 0; - std::snprintf(out, out_size, "%s", g_cpu_mode_option.c_str()); - return 1; -} +/* ---- 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; } -/* ---- one session start, as main.cpp sequences it ------------------------ */ +/* ---- the runtime surface mod_runtime.cpp links against ------------------- */ -struct Baseline { - int geometry = 0, texture = 0, cpu_mode = 0; - const char* env_geometry = nullptr; - const char* env_texture = nullptr; - const char* env_cpu_mode = nullptr; -}; +static std::array 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) {} -static PSXPgxpSessionArm run_session(bool mod_planned, const Baseline& b) { - pgxp_mod_request(0, 0); /* reset_mod_owned_presentation */ - if (mod_planned && g_activation) g_activation(); /* mod_runtime_activate_plugins */ - PSXPgxpSessionInputs in{}; - in.env_geometry = psx_pgxp_session_env_flag(b.env_geometry); - in.env_texture = psx_pgxp_session_env_flag(b.env_texture); - in.env_cpu_mode = psx_pgxp_session_env_flag(b.env_cpu_mode); - in.video_geometry = b.geometry; - in.video_texture = b.texture; - in.video_cpu_mode = b.cpu_mode; - in.mod_enabled = pgxp_mod_requested(&in.mod_cpu_mode); - return psx_pgxp_session_resolve(&in); +/* ---- 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 bool arm_is(const PSXPgxpSessionArm& a, int g, int t, int c) { - return a.geometry == g && a.texture == t && a.cpu_mode == c; +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 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) { - /* The builtin registered itself at load, under the plugin id its - * manifest names. */ - CHECK(g_activation != nullptr); - CHECK(g_activation_id == "psx.pgxp"); + 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 Baseline off; /* every [video] PGXP key at its default */ - - /* Default: no mod, no keys -> the faithful floor. */ - CHECK(arm_is(run_session(false, off), 0, 0, 0)); - - /* The enabled mod arms both corrections even though the baseline - * applied after activation says off (the pre-G1.11 bug). */ - CHECK(arm_is(run_session(true, off), 1, 1, 0)); - - /* Activation only records the request; it does not arm the engine - * itself (the renderer setup does, from the resolved values). */ - pgxp_set_enabled(0); - pgxp_mod_request(0, 0); - g_activation(); + 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"); { - int cpu = -1; - CHECK(pgxp_mod_requested(&cpu) == 1 && cpu == 0); - CHECK(pgxp_enabled() == 0); + 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 mod's CPU-mode option rides along. */ - g_cpu_mode_option = "true"; - CHECK(arm_is(run_session(true, off), 1, 1, 1)); - g_cpu_mode_option = "false"; + /* 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); - /* Netplay clears the plan: nothing activates, so the session keeps - * exactly the baseline, even right after a session that had the mod. */ - CHECK(arm_is(run_session(true, off), 1, 1, 0)); - CHECK(arm_is(run_session(false, off), 0, 0, 0)); - { - int cpu = -1; - CHECK(pgxp_mod_requested(&cpu) == 0 && cpu == 0); - } + /* 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)); - /* A player's [video] keys still work without the mod, and the mod adds - * to them rather than replacing them. */ - { - Baseline tex_only; - tex_only.texture = 1; - CHECK(arm_is(run_session(false, tex_only), 0, 1, 0)); - CHECK(arm_is(run_session(true, tex_only), 1, 1, 0)); - Baseline cpu_only; - cpu_only.cpu_mode = 1; - CHECK(arm_is(run_session(false, cpu_only), 0, 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"); - /* Validation env overrides win over both: an A/B arm can switch PGXP - * off under an enabled mod, or on without one. */ - { - Baseline env_off; - env_off.env_geometry = "0"; - env_off.env_texture = "0"; - CHECK(arm_is(run_session(true, env_off), 0, 0, 0)); - Baseline env_on; - env_on.env_geometry = "1"; - CHECK(arm_is(run_session(false, env_on), 1, 0, 0)); - Baseline env_empty; /* set but empty = off, the runtime's old rule */ - env_empty.env_texture = ""; - CHECK(arm_is(run_session(true, env_empty), 1, 0, 0)); - } CHECK(psx_pgxp_session_env_flag(nullptr) == -1); CHECK(psx_pgxp_session_env_flag("0") == 0); CHECK(psx_pgxp_session_env_flag("") == 0);