diff --git a/docs/ENHANCEMENTS.md b/docs/ENHANCEMENTS.md index 91fe4076..a9b03031 100644 --- a/docs/ENHANCEMENTS.md +++ b/docs/ENHANCEMENTS.md @@ -882,10 +882,11 @@ the formal Crash/Tomba2 A/B. ### G1.11 — PGXP as a title's default: hook build, mod at session start, dataflow only, exact projection (2026-10-01) Ridge Racer Type 4 turns PGXP on by default. Getting there took framework -changes that are all opt-in. With PGXP off, nothing changes for any title. -With PGXP on, two changes apply without a new option: the -`psx.enhancement.pgxp` mod now actually arms PGXP (item 2), and fully precise -axis-aligned quads take the triangle path (item 4). +changes within the elective PGXP enhancement. With PGXP off, nothing changes +for any title. With PGXP on, its correctness fixes apply without a new option: +the mod arms PGXP (item 2), hook builds default to dataflow-only precision +without a tolerance clamp (item 3), and fully precise axis-aligned quads take +the triangle path (item 4). **What R4 got from G1.10 as shipped: nothing.** Base flavor, PGXP mod on: 0% dataflow hits, 3.6% position-cache hits, 0 of 282k textured triangles with @@ -921,12 +922,18 @@ sub-vertex with RTPT. own manifest (default-on override, netplay clear with and without the reset, the off switch, options, env overrides). R4's `r4_pgxp_boot` boots the built runtime and fails if main.cpp stops calling the arming. -3. **Two `[video]` keys** (game.toml; live over the TCP `pgxp` verb): +3. **Automatic hook-build defaults and `[video]` overrides** (game.toml; + live over the TCP `pgxp` verb). A runtime built with `PSX_PGXP=1` selects + `pgxp_tolerance = -1.0` and `pgxp_position_fallback = false` when those keys + are absent. The runtime and overlay ABI enforce the matching hook flavor. + Base builds keep 0.5 and true for their limited dataflow coverage. Explicit + title tuning wins independently for each key, including Ape's validated + clamp. PGXP remains elective; exact-projection shadows remain separate: ```toml [video] - pgxp_tolerance = -1.0 # existing key: no clamp - pgxp_position_fallback = false # default true + pgxp_tolerance = -1.0 # hook-build default: no clamp + pgxp_position_fallback = false # hook-build default: dataflow only pgxp_preserve_projection = true # default false ``` @@ -957,8 +964,10 @@ sub-vertex with RTPT. (the G1.10 Ape result). At hook-build coverage the clamp is harmful: on R4 it keeps 26% of vertices, leaves 52% of drawn triangles mixed (a 4K capture shows a hairline across the road) and shakes more than stock - (tables below). The default stays 0.5 because changing it would change - every title that already arms PGXP through `[video]`. + (tables below). The old universal default stayed 0.5 to avoid changing + every title that already arms PGXP through `[video]`. This historical + default is now retained only in base builds; hook builds automatically + disable the clamp unless a title supplies an explicit value. 4. **Fully precise axis-aligned textured quads skip the 2D rectangle shortcut.** `gp0_exec_textured_quad` draws a quad whose integer vertices and UVs form an axis-aligned rectangle with `gr_draw_textured_rect`, at the diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index b1aa2101..2141a928 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -e7c8c626e955b89839c5dda1d9990857d87a0042a1cdc1071bbdc42b754a4146 +85d8625c20a9c7936292a085c0b85d6899dfa1011d8e8754167cdf5dcedbf330 diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index d050bcb8..3024b8ea 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -812,10 +812,12 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& if (video.contains("pgxp_tolerance")) { rt.video_pgxp_tolerance = toml_number(video, "pgxp_tolerance"); + rt.video_pgxp_tolerance_set = true; } if (video.contains("pgxp_position_fallback")) { rt.video_pgxp_position_fallback = toml::find(video, "pgxp_position_fallback"); + rt.video_pgxp_position_fallback_set = true; } if (video.contains("pgxp_preserve_projection")) { rt.video_pgxp_preserve_projection = diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 5c183a84..1cefbd03 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -529,22 +529,24 @@ struct RuntimeConfig { // pgxp_tolerance: reject a corrected vertex whose sub-pixel offset from // the native integer position exceeds this many pixels (the truncation- // agreement check already bounds offsets to < 1px; this narrows them - // further). Default 0.5 — user-validated on Ape Escape (2026-08-15): + // further). Unspecified: off in the PGXP hook flavor, 0.5 in base builds. + // An explicit title value wins — user-validated on Ape Escape (2026-08-15): // unclamped, sparse hairline background-bleed seams appear where a // corrected triangle borders an uncorrected one; at 0.5 the seams are // gone and only sub-half-pixel misalignment remains. Negative disables // the clamp. Live-tunable over TCP (pgxp verb). double video_pgxp_tolerance = 0.5; + bool video_pgxp_tolerance_set = false; // pgxp_position_fallback: let a vertex with no validated dataflow shadow // take the fraction of the projection last cached at its integer screen - // position (the G1.4 exact table). Default true (unchanged behaviour). - // A title built with the PGXP hooks (psxrecomp_add_game_runtime PGXP) - // reaches near-total dataflow coverage, so for it the cache only hands - // unrelated fractions to CPU-built 2D polygons; such a title sets false - // ("dataflow only", the reference implementations' default). + // position (the G1.4 exact table). Unspecified: false in the PGXP hook + // flavor, true in base builds. The hook flavor tracks full-word copies; + // the cache can assign unrelated 3D fractions to CPU-built 2D polygons. + // An explicit title value overrides the runtime's automatic default. // Live-tunable over TCP (pgxp verb). docs/ENHANCEMENTS.md G1.11. bool video_pgxp_position_fallback = true; + bool video_pgxp_position_fallback_set = false; // pgxp_preserve_projection: shadow the exact projection of each vertex // (from the GTE's unshifted MACs and a true divide) instead of the GTE's diff --git a/recompiler/tests/video_enhancement_settings_test.cpp b/recompiler/tests/video_enhancement_settings_test.cpp index eeedc5a4..33dda58c 100644 --- a/recompiler/tests/video_enhancement_settings_test.cpp +++ b/recompiler/tests/video_enhancement_settings_test.cpp @@ -72,6 +72,9 @@ static void test_defaults_off() { "geometry_correction defaults OFF (known to crack meshes)"); check(!gc.runtime.video_perspective_texturing, "perspective_texturing defaults OFF (faithful floor; opt-in)"); + check(!gc.runtime.video_pgxp_tolerance_set && + !gc.runtime.video_pgxp_position_fallback_set, + "absent PGXP tuning delegates defaults to the runtime flavor"); fs::remove(p); } @@ -427,16 +430,15 @@ static void test_frame_generation() { fs::remove(q); } -/* docs/ENHANCEMENTS.md G1.11: the PGXP title keys. Defaults keep the - * historical behaviour (tolerance 0.5, position cache consulted, IR-path - * shadows); a title built with the hooks sets all three. */ +/* PGXP title tuning retains base values here; presence flags distinguish + * explicit title overrides from the runtime's hook-flavor defaults. */ static void test_pgxp_title_keys() { fs::path p = write_game_toml("psxrecomp_pgxp_keys_default.toml", ""); auto gc = PSXRecompV4::load_game_config(p); check(gc.runtime.video_pgxp_tolerance == 0.5, - "pgxp_tolerance defaults to 0.5"); + "pgxp_tolerance retains the base-build value before runtime resolution"); check(gc.runtime.video_pgxp_position_fallback, - "pgxp_position_fallback defaults ON (unchanged behaviour)"); + "pgxp_position_fallback retains the base value before runtime resolution"); check(!gc.runtime.video_pgxp_preserve_projection, "pgxp_preserve_projection defaults OFF (unchanged behaviour)"); check(!gc.runtime.video_pgxp_mod_only, @@ -465,8 +467,9 @@ static void test_pgxp_title_keys() { "pgxp_tolerance = 1\n" "scanline_strength = 1\n"); gc = PSXRecompV4::load_game_config(p); - check(gc.runtime.video_pgxp_depth_threshold == 0.0 && gc.runtime.video_pgxp_tolerance == 1.0, - "integer literals load for number keys"); + check(gc.runtime.video_pgxp_depth_threshold == 0.0 && gc.runtime.video_pgxp_tolerance == 1.0 && + gc.runtime.video_pgxp_tolerance_set && !gc.runtime.video_pgxp_position_fallback_set, + "explicit tolerance alone leaves fallback on its automatic default"); fs::remove(p); p = write_game_toml("psxrecomp_pgxp_render_bad.toml", @@ -487,8 +490,10 @@ static void test_pgxp_title_keys() { "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_tolerance < 0.0 && + gc.runtime.video_pgxp_tolerance_set && + gc.runtime.video_pgxp_position_fallback_set, + "explicit PGXP tuning is marked as an override"); check(!gc.runtime.video_pgxp_position_fallback, "pgxp_position_fallback = false is honoured"); check(gc.runtime.video_pgxp_preserve_projection, diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 06036ec6..5050ea17 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -724,32 +724,38 @@ if(BUILD_TESTING) # 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/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/mod_media.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) + # Run the same session fixture in both real compilation flavors. + foreach(_pgxp_session_test IN ITEMS pgxp_session_test pgxp_session_hooks_test) + add_executable(${_pgxp_session_test} tests/test_pgxp_session.cpp + 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/mod_media.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() - endif() - add_test(NAME pgxp_session_test COMMAND pgxp_session_test) + add_test(NAME ${_pgxp_session_test} COMMAND ${_pgxp_session_test}) + if(_pgxp_session_test STREQUAL "pgxp_session_hooks_test") + target_compile_definitions(${_pgxp_session_test} PRIVATE PSX_PGXP=1) + endif() + endforeach() add_executable(psx_instr_cost_test tests/test_psx_instr_cost.cpp) target_include_directories(psx_instr_cost_test PRIVATE include) diff --git a/runtime/include/pgxp_session.h b/runtime/include/pgxp_session.h index 9cf58790..f1b43ca5 100644 --- a/runtime/include/pgxp_session.h +++ b/runtime/include/pgxp_session.h @@ -105,6 +105,10 @@ typedef struct PSXPgxpSessionConfig { int video_cpu_mode; float tolerance; int position_fallback; + /* Set only for explicit [video] values. Otherwise the runtime chooses + * no clamp / dataflow-only in hook builds, 0.5 / cache in base builds. */ + int tolerance_set; + int position_fallback_set; int preserve_projection; int mod_only; int netplay; /* this session is a netplay match: precise culling off */ diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index c593f577..eeaf87b7 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -1277,8 +1277,10 @@ static int g_video_geometry_correction = 0; static int g_video_perspective_texturing = 0; static int g_video_pgxp_cpu_mode = 0; static float g_video_pgxp_tolerance = 0.5f; -/* docs/ENHANCEMENTS.md G1.11: dataflow-only and exact-projection shadows. - * Defaults keep the historical behaviour. game.toml [video] only. */ +/* game.toml [video] tuning. Session arming selects dataflow-only/no-clamp + * defaults in hook builds unless the title supplied an explicit value. */ +static int g_video_pgxp_tolerance_set = 0; +static int g_video_pgxp_position_fallback_set = 0; static int g_video_pgxp_position_fallback = 1; static int g_video_pgxp_preserve_projection = 0; /* PGXP renderer features (G1.14); PSX_PGXP_DEPTH / _COLOR / _SEAM override. */ @@ -16098,6 +16100,9 @@ int main(int argc, char** argv) { gc.runtime.video_perspective_texturing ? 1 : 0; g_video_pgxp_cpu_mode = gc.runtime.video_pgxp_cpu_mode ? 1 : 0; g_video_pgxp_tolerance = (float)gc.runtime.video_pgxp_tolerance; + g_video_pgxp_tolerance_set = gc.runtime.video_pgxp_tolerance_set; + g_video_pgxp_position_fallback_set = + gc.runtime.video_pgxp_position_fallback_set; g_video_pgxp_position_fallback = gc.runtime.video_pgxp_position_fallback ? 1 : 0; g_video_pgxp_preserve_projection = @@ -18499,6 +18504,8 @@ session_reboot: 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.tolerance_set = g_video_pgxp_tolerance_set; + pgxp_cfg.position_fallback_set = g_video_pgxp_position_fallback_set; pgxp_cfg.preserve_projection = g_video_pgxp_preserve_projection; pgxp_cfg.mod_only = g_video_pgxp_mod_only; /* Precise culling changes guest NCLIP results from host-only shadows; a diff --git a/runtime/src/mod_builtin_pgxp.c b/runtime/src/mod_builtin_pgxp.c index cc7baf1c..b973fd34 100644 --- a/runtime/src/mod_builtin_pgxp.c +++ b/runtime/src/mod_builtin_pgxp.c @@ -7,7 +7,8 @@ * (the faithful floor, docs/ENHANCEMENTS.md G1.9); enabling arms geometry and * 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. + * keys still apply. Unspecified tuning is dataflow-only/no-clamp in hook + * builds, or position-cache/0.5px in base builds (pgxp_session.cpp). * * Activation only RECORDS the request (pgxp_mod_request). It runs before the * renderer setup in main.cpp, whose session arming (pgxp_session.cpp) takes diff --git a/runtime/src/pgxp_session.cpp b/runtime/src/pgxp_session.cpp index 752ce469..76833e9d 100644 --- a/runtime/src/pgxp_session.cpp +++ b/runtime/src/pgxp_session.cpp @@ -34,8 +34,21 @@ extern "C" PSXPgxpSessionArm psx_pgxp_session_arm( 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); + /* The hook flavor tracks the full-word dataflow. A coordinate cache can + * inject unrelated world fractions into HUD vertices, and a tolerance + * clamp discards valid shadows, mixing precise/native triangle corners. + * Base builds lack that coverage and retain their validated defaults. + * Neither default enables the optional PGXP enhancement itself. */ +#if defined(PSX_PGXP) && PSX_PGXP + const float default_tolerance = -1.0f; + const int default_position_fallback = 0; +#else + const float default_tolerance = 0.5f; + const int default_position_fallback = 1; +#endif + pgxp_set_tolerance(config->tolerance_set ? config->tolerance : default_tolerance); + pgxp_set_position_fallback(config->position_fallback_set ? + config->position_fallback : default_position_fallback); pgxp_set_preserve_projection(config->preserve_projection); pgxp_set_culling(arm.culling); gte_geometry_correction_set(arm.geometry); diff --git a/runtime/tests/test_pgxp_session.cpp b/runtime/tests/test_pgxp_session.cpp index d201ba52..ad6529f3 100644 --- a/runtime/tests/test_pgxp_session.cpp +++ b/runtime/tests/test_pgxp_session.cpp @@ -29,6 +29,7 @@ #include "cpu_state.h" #include "gpu.h" +#include "gpu_hd_textures.h" #include #include @@ -76,11 +77,22 @@ static int g_failures = 0; static int g_geometry = -1; static int g_texture = -1; +extern "C" int psx_netplay_active(void) { return 0; } +/* No external HD pack is configured by the PGXP session fixture. */ +extern "C" void gpu_hd_textures_shutdown(void) {} +extern "C" int gpu_hd_textures_configure(const char*, int, int, char*, size_t) { return 0; } +extern "C" int gpu_hd_textures_reload(char*, size_t) { return 0; } +extern "C" void gpu_hd_textures_set_dump_enabled(int) {} +extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) { *out = GpuHdTextureDiag{}; } 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; +static bool g_cached_hud_vertex = false; +extern "C" int gte_geometry_correction_lookup(uint32_t, int32_t* x, int32_t* y) { + if (!g_cached_hud_vertex) return 0; + *x = (160 << 16) + 0x4000; + *y = (80 << 16) + 0x4000; + return 1; } /* The session harness has no GTE backend; pre-draw probes must link as a no-op. */ @@ -219,6 +231,70 @@ int main(void) { "PSX_PGXP_CPU_MODE", "PSX_PGXP_CULLING"}) test_unsetenv(v); g_cfg = PSXPgxpSessionConfig{}; + /* No title tuning: session arming must choose the build's defaults, + * including resetting values left by a previous session/debug toggle. */ + pgxp_set_tolerance(2.0f); + pgxp_set_position_fallback(0); + CHECK(armed(session(), 0, 0, 0, 0)); +#if defined(PSX_PGXP) && PSX_PGXP + CHECK(pgxp_tolerance() < 0.0f && pgxp_position_fallback() == 0); +#else + CHECK(pgxp_tolerance() == 0.5f && pgxp_position_fallback() == 1); +#endif + CHECK(pgxp_preserve_projection() == 0); + /* Run those defaults through the real engine: a proven world vertex + * retains its 0.75px fraction, while a HUD coordinate sharing its integer + * position cannot borrow a different projection from the cache. */ + pgxp_set_enabled(1); + const uint32_t packed = (80u << 16) | 160u; + const uint32_t addr = 0x80100000u; + const int32_t precise_x = (160 << 16) + 0xc000; + pgxp_gte_push_sxy(precise_x, 80 << 16, 100, packed); + psx_pgxp_cop2(nullptr, (0x3au << 26) | (14u << 16), packed, addr); + int32_t x, y; + uint16_t z; + const int world = pgxp_get_precise_vertex(addr, packed, 160, 80, &x, &y, &z); +#if defined(PSX_PGXP) && PSX_PGXP + CHECK(world == PGXP_SRC_DATAFLOW && x == precise_x && z == 100); +#else + CHECK(world == PGXP_SRC_NATIVE && x == (160 << 16) && z == 100); +#endif + g_cached_hud_vertex = true; + const int hud = pgxp_get_precise_vertex(0xffffffffu, packed, 160, 80, &x, &y, &z); +#if defined(PSX_PGXP) && PSX_PGXP + CHECK(hud == PGXP_SRC_NATIVE && x == (160 << 16) && z == 0); +#else + CHECK(hud == PGXP_SRC_FALLBACK && x == (160 << 16) + 0x4000 && z == 0); +#endif + g_cached_hud_vertex = false; + pgxp_set_enabled(0); + /* Each explicit compatibility override is independent of the other. */ + g_cfg.tolerance = 0.25f; + g_cfg.tolerance_set = 1; + CHECK(armed(session(), 0, 0, 0, 0)); + CHECK(pgxp_tolerance() == 0.25f); +#if defined(PSX_PGXP) && PSX_PGXP + CHECK(pgxp_position_fallback() == 0); +#else + CHECK(pgxp_position_fallback() == 1); +#endif + g_cfg.tolerance_set = 0; + g_cfg.position_fallback = 1; + g_cfg.position_fallback_set = 1; + CHECK(armed(session(), 0, 0, 0, 0)); +#if defined(PSX_PGXP) && PSX_PGXP + CHECK(pgxp_tolerance() < 0.0f); +#else + CHECK(pgxp_tolerance() == 0.5f); +#endif + CHECK(pgxp_position_fallback() == 1); + /* Explicit compatibility tuning wins even in the hook flavor. */ + g_cfg.tolerance = 0.5f; + g_cfg.tolerance_set = 1; + g_cfg.position_fallback = 1; + g_cfg.position_fallback_set = 1; + CHECK(armed(session(), 0, 0, 0, 0)); + CHECK(pgxp_tolerance() == 0.5f && pgxp_position_fallback() == 1); g_cfg.tolerance = -1.0f; g_cfg.position_fallback = 0; g_cfg.preserve_projection = 1;