From 3787120488cf343bb167b87b18e5b614aa752256 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 2 Oct 2026 16:00:02 -0700 Subject: [PATCH] Recover saturated horizontal projections in opt-in native-wide rendering --- docs/NATIVE-WIDE-PROJECTION.md | 35 ++++++++ runtime/CMakeLists.txt | 5 +- runtime/include/gpu.h | 1 + runtime/include/mod_plugins.h | 5 ++ runtime/src/debug_server.c | 11 ++- runtime/src/gpu.c | 50 ++++++++++- runtime/tests/gpu_exec_stubs.inc | 24 +++++- runtime/tests/test_native_wide_projection.c | 91 +++++++++++++++++++++ runtime/tests/test_pgxp.cpp | 15 ++++ 9 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 docs/NATIVE-WIDE-PROJECTION.md create mode 100644 runtime/tests/test_native_wide_projection.c diff --git a/docs/NATIVE-WIDE-PROJECTION.md b/docs/NATIVE-WIDE-PROJECTION.md new file mode 100644 index 00000000..17de84c8 --- /dev/null +++ b/docs/NATIVE-WIDE-PROJECTION.md @@ -0,0 +1,35 @@ +# Native-wide projection correction + +Trusted title plugins can call +`psx_mod_set_native_wide_projection_correction(1)` to recover horizontally +saturated GTE vertices during native-wide rendering. The default is disabled. +It is independent of the user-facing geometry and perspective-texture options. + +The PS1 clamps projected screen X to -1024 through 1023. Two different vertices +can therefore collapse onto one X edge, folding a planar quad when a wider view +reveals its triangles. The renderer recovers the original horizontal projection +from the existing PGXP transport only when the packet address and complete word +match, depth is present, the integer Y still agrees, and X actually extends past +the corresponding saturation edge. Missing or stale provenance stays stock. +The bounded projection transport's own +/-4096 endpoints are rejected. + +This changes host triangle positions only. Guest SXY registers, packet words, +vertical coordinates, NCLIP, collision and ordering-table behavior stay intact. +With zero native-wide reveal, including a selected 4:3 view, correction is +inactive. Existing geometry correction can still supply subpixel coordinates +for ordinary vertices when the user enables it separately. + +`ws_nw projection_correction=0|1` provides a diagnostic A/B toggle. The reply +reports the selected policy and the cumulative corrected vertex count, reset +on a policy change. It does not toggle geometry or texture correction. + +The executable GPU regression uses a captured folding GT4 shape, verifies both +triangle windings after recovery, and checks unchanged packet/Y data, both +saturation edges, zero reveal, disabling the policy, missing depth, stale words, +wrong addresses and transport endpoints. The PGXP regression separately verifies +that a saturated MFC2/store retains its exact extended projection while the +ordinary conservative geometry consumer continues to reject it. + +This policy cannot recover a projection whose provenance has already been lost +through unsupported CPU arithmetic, or repair a guest culling decision made +before GPU submission. Those paths still need title-specific qualification. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 56a208d9..54cb588d 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -449,7 +449,9 @@ if(BUILD_TESTING) add_executable(ws_view_anchor_test tests/test_ws_view_anchor.c) add_executable(ws_view_anchor_gpu_test tests/test_ws_view_anchor_gpu.c src/psx_video_timing.c) - foreach(_anchor_test ws_view_anchor ws_view_anchor_gpu) + add_executable(native_wide_projection_test tests/test_native_wide_projection.c + src/psx_video_timing.c) + foreach(_anchor_test ws_view_anchor ws_view_anchor_gpu native_wide_projection) target_include_directories(${_anchor_test}_test PRIVATE include src) if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(${_anchor_test}_test PRIVATE -ffunction-sections -fdata-sections) @@ -463,6 +465,7 @@ if(BUILD_TESTING) endforeach() add_test(NAME ws_view_anchor_test COMMAND ws_view_anchor_test) add_test(NAME ws_view_anchor_gpu_test COMMAND ws_view_anchor_gpu_test) + add_test(NAME native_wide_projection_test COMMAND native_wide_projection_test) add_executable(ws_repeat_rect_test tests/test_ws_repeat_rect.c) target_include_directories(ws_repeat_rect_test PRIVATE include) diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 5dac3b27..a5890d0c 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -456,6 +456,7 @@ void gpu_ws_restore_linked_list_rank(uint32_t rank); * gradient / backdrop image) to fill the wide frame, so it no longer * pillarboxes at the reveal margins. Runtime-only. Off by default. */ void gpu_ws_set_nw_backdrop(int on); +int gpu_ws_native_wide_projection_correction(uint64_t *vertices); /* Native-wide flat-polygon backdrop stretch ([widescreen] nw_flat_backdrop): * stretch untextured primitives in the wide mirror without changing the * canonical 4:3 framebuffer. Intended for flat-colour sky/water backdrops. */ diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 2bb49d20..d6b7b987 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -109,6 +109,11 @@ int psx_mod_set_main_ram_8mb(int enabled); /* Current per-side widescreen reveal in native game pixels (zero at 4:3). */ int32_t psx_mod_widescreen_x_margin(void); +/* Opt into render-only recovery of saturated horizontal GTE projections in + * native-wide gameplay. Requires exact packet-address/word provenance and + * depth; never changes guest SXY, vertical coordinates, or the 4:3 path. */ +void psx_mod_set_native_wide_projection_correction(int enabled); + /* Mark a guest GPU packet (P_TAG address) as persistent screen-space HUD. * edge = -1 left, +1 right, 0 clears a reused packet's tag. The native-wide * compositor translates it by the live reveal, excluding culling guards. diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 9b236be9..ebd1a17c 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8073,10 +8073,17 @@ static void handle_ws_nw(int id, const char *json) { int on = json_get_int(json, "on", -1); if (on >= 0) psx_ws_set_native_wide(on); + int correction = json_get_int(json, "projection_correction", -1); + if (correction >= 0) + psx_mod_set_native_wide_projection_correction(correction); + uint64_t corrected_vertices = 0; + correction = gpu_ws_native_wide_projection_correction(&corrected_vertices); GpuWsDebug ws; gpu_ws_get_debug(&ws); - send_fmt("{\"id\":%d,\"ok\":true,\"native_wide\":%d,\"mode\":%d,\"nw_extra\":%d}", - id, psx_ws_get_native_wide(), ws.mode, ws.nw_extra); + send_fmt("{\"id\":%d,\"ok\":true,\"native_wide\":%d,\"mode\":%d,\"nw_extra\":%d," + "\"projection_correction\":%d,\"projection_vertices\":%llu}", + id, psx_ws_get_native_wide(), ws.mode, ws.nw_extra, + correction, (unsigned long long)corrected_vertices); } /* Live scanline post-process toggle (A/B): `scanline on=<0|1> pct=<0..100>`. diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index b30837a6..0050e433 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -3943,6 +3943,8 @@ static void parse_vertex(uint32_t word, int32_t* x, int32_t* y) { extern int gte_geometry_correction_enabled(void); static int s_texture_correction_enabled = 0; +static int s_native_wide_projection_correction = 0; +static uint64_t s_native_wide_projection_vertices = 0; extern int gte_precision_load_word(uint32_t addr, uint32_t packed, int32_t *x16, int32_t *y16, uint16_t *z); @@ -3951,7 +3953,44 @@ extern int gte_precision_load_word(uint32_t addr, uint32_t packed, void gpu_pgxp_rederive_enable(void) { pgxp_set_enabled(s_texture_correction_enabled || gte_geometry_correction_enabled() || - ws_precise_nclip_cfg); + ws_precise_nclip_cfg || + s_native_wide_projection_correction); +} + +void psx_mod_set_native_wide_projection_correction(int enabled) { + s_native_wide_projection_correction = enabled ? 1 : 0; + s_native_wide_projection_vertices = 0; + gpu_pgxp_rederive_enable(); +} + +int gpu_ws_native_wide_projection_correction(uint64_t *vertices) { + if (vertices) *vertices = s_native_wide_projection_vertices; + return s_native_wide_projection_correction; +} + +/* The PS1 clamps each projected X independently. At a wider FOV that can + * bend an otherwise planar quad, even while both clamped vertices remain + * outside the viewport. Recover only a proven saturated projection. An + * ordinary CPU coordinate, a modified packet, and a missing depth stay stock. + * The +/-4096 transport endpoints themselves are clamped, so reject them. */ +static int native_wide_projection_x(uint32_t addr, uint32_t word, + int32_t raw_x, int32_t raw_y, + int32_t *x16) { + if (!s_native_wide_projection_correction || ws_nw_extra() <= 0 || + (raw_x != -1024 && raw_x != 1023) || addr == UINT32_MAX) + return 0; + int32_t px, py; + uint16_t z; + if (!gte_precision_load_word(addr, word, &px, &py, &z) || !z || + (py >> 16) != raw_y || px <= -4096 * 65536 || + px >= 4096 * 65536 - 1) + return 0; + if ((raw_x == -1024 && (px >> 16) >= -1024) || + (raw_x == 1023 && (px >> 16) <= 1023)) + return 0; + *x16 = px; + s_native_wide_projection_vertices++; + return 1; } void gpu_texture_correction_set(int enabled) { @@ -3979,7 +4018,8 @@ uint32_t gpu_texture_correction_hits(void) { static void prepare_precise_triangle(int i0, int i1, int i2, const int32_t vx[3], const int32_t vy[3]) { gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f); - if (!gte_geometry_correction_enabled()) { + const int geometry = gte_geometry_correction_enabled(); + if (!geometry && !s_native_wide_projection_correction) { gr_set_precise_triangle(0, 0,0, 0,0, 0,0); return; } @@ -3995,9 +4035,13 @@ static void prepare_precise_triangle(int i0, int i1, int i2, : gp0_cmd_source_addr + (uint32_t)idx[i] * 4u; int32_t px, py; uint16_t sz; - if (pgxp_get_precise_vertex(addr, word, raw_x, raw_y, + px = raw_x * 65536; + py = raw_y * 65536; + if (geometry && pgxp_get_precise_vertex(addr, word, raw_x, raw_y, &px, &py, &sz) != PGXP_SRC_NATIVE) any_precise = 1; + if (native_wide_projection_x(addr, word, raw_x, raw_y, &px)) + any_precise = 1; fx[i] = (int32_t)((int64_t)px + (int64_t)(vx[i] - raw_x) * 65536); fy[i] = (int32_t)((int64_t)py + (int64_t)(vy[i] - raw_y) * 65536); } diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index 72d6fb72..1cfd7c00 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -94,7 +94,8 @@ void psx_fatal_halt(const char *reason) { assert(!"psx_fatal_halt called"); } -void pgxp_set_enabled(int enabled) { (void)enabled; } +static int fixture_pgxp_enabled; +void pgxp_set_enabled(int enabled) { fixture_pgxp_enabled = enabled; } int gte_geometry_correction_enabled(void) { return 0; } int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word, int32_t int_x, int32_t int_y, @@ -103,9 +104,23 @@ int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word, (void)x16; (void)y16; (void)sz; return PGXP_SRC_NATIVE; } +static struct { + uint32_t addr, word; + int32_t x16, y16; + uint16_t z; + int valid; +} fixture_projections[4]; int gte_precision_load_word(uint32_t addr, uint32_t packed, int32_t *x16, int32_t *y16, uint16_t *z) { - (void)addr; (void)packed; (void)x16; (void)y16; (void)z; + for (unsigned i = 0; i < 4; ++i) { + if (fixture_projections[i].valid && fixture_projections[i].addr == addr && + fixture_projections[i].word == packed) { + if (x16) *x16 = fixture_projections[i].x16; + if (y16) *y16 = fixture_projections[i].y16; + if (z) *z = fixture_projections[i].z; + return fixture_projections[i].z != 0; + } + } return 0; } uint32_t sw_perspective_triangle_count(void) { return 0; } @@ -189,9 +204,12 @@ void gr_set_texture_window(uint32_t raw) { (void)raw; } void gr_set_color_modulation(int r, int g, int b, int raw_texture) { (void)r; (void)g; (void)b; (void)raw_texture; } +static struct { int enabled; int32_t xy[6]; } fixture_precise_triangle; void gr_set_precise_triangle(int enabled, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t x2, int32_t y2) { - (void)enabled; (void)x0; (void)y0; (void)x1; (void)y1; (void)x2; (void)y2; + fixture_precise_triangle.enabled = enabled; + int32_t xy[6] = { x0, y0, x1, y1, x2, y2 }; + memcpy(fixture_precise_triangle.xy, xy, sizeof xy); } void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2) { (void)enabled; (void)q0; (void)q1; (void)q2; diff --git a/runtime/tests/test_native_wide_projection.c b/runtime/tests/test_native_wide_projection.c new file mode 100644 index 00000000..4d1e04d2 --- /dev/null +++ b/runtime/tests/test_native_wide_projection.c @@ -0,0 +1,91 @@ +/* Captured MediEvil GT4 regression: saturating two different X projections + * reverses the second triangle. Execute the GPU preparation path and verify + * correction, unchanged packets/Y, and exact-provenance fallback conditions. */ +#define main gpu_fixture_main +#include "test_gpu_textured_dot_nw_shift_exec.c" +#undef main + +static int64_t area(const int32_t *p) { + int64_t x0=p[0]/65536, y0=p[1]/65536; + int64_t x1=p[2]/65536, y1=p[3]/65536; + int64_t x2=p[4]/65536, y2=p[5]/65536; + return (x1-x0)*(y2-y0)-(y1-y0)*(x2-x0); +} + +int main(void) { + reset_gpu_state_for_test(); + configure_native_wide_16_9(); + hres1=2; ws_cfg_num=125; ws_cfg_den=30; /* 512 pixels, owner's ultrawide view */ + gp0_cmd_source_addr=0x10004u; + const int indices[4]={1,4,7,10}; + const int32_t x[4]={882,988,1023,1023}, y[4]={117,130,152,173}; + const int32_t projected[4]={882,988,1040,1203}; + for (unsigned i=0;i<4;i++) { + uint32_t word=pack_vertex(x[i],y[i]); + gp0_cmd_buf[indices[i]]=word; + fixture_projections[i].addr=gp0_cmd_source_addr+indices[i]*4; + fixture_projections[i].word=word; + fixture_projections[i].x16=projected[i]*65536; + fixture_projections[i].y16=y[i]*65536+17000; + fixture_projections[i].z=900; + fixture_projections[i].valid=1; + } + uint32_t original[12];memcpy(original,gp0_cmd_buf,sizeof original); + const int32_t ax[3]={882,988,1023}, ay[3]={117,130,152}; + const int32_t bx[3]={1023,988,1023}, by[3]={152,130,173}; + const int32_t folded[6]={1023*65536,152*65536,988*65536,130*65536, + 1023*65536,173*65536}; + assert(area(folded)==-735); + prepare_precise_triangle(1,4,7,ax,ay); + assert(!fixture_precise_triangle.enabled); /* default remains stock */ + psx_mod_set_native_wide_projection_correction(1); + assert(fixture_pgxp_enabled); + prepare_precise_triangle(1,4,7,ax,ay); + assert(fixture_precise_triangle.enabled && area(fixture_precise_triangle.xy)>0); + assert(fixture_precise_triangle.xy[5]==152*65536); /* no Y fraction correction */ + prepare_precise_triangle(7,4,10,bx,by); + assert(fixture_precise_triangle.enabled && area(fixture_precise_triangle.xy)>0); + assert(!memcmp(original,gp0_cmd_buf,sizeof original)); + const int32_t shifted_x[3]={1030,995,1030}, shifted_y[3]={149,127,170}; + prepare_precise_triangle(7,4,10,shifted_x,shifted_y); + assert(fixture_precise_triangle.xy[4]==1210*65536); + assert(fixture_precise_triangle.xy[5]==170*65536); + uint64_t recovered=0; + assert(gpu_ws_native_wide_projection_correction(&recovered) && recovered>0); + int32_t px=0; + uint32_t addr=fixture_projections[2].addr, word=fixture_projections[2].word; + assert(native_wide_projection_x(addr,word,1023,152,&px)); + assert(!native_wide_projection_x(addr+4,word,1023,152,&px)); + assert(!native_wide_projection_x(addr,word^1,1023,152,&px)); + assert(!native_wide_projection_x(UINT32_MAX,word,1023,152,&px)); + assert(!native_wide_projection_x(addr,word,1000,152,&px)); + fixture_projections[2].z=0; + assert(!native_wide_projection_x(addr,word,1023,152,&px)); + fixture_projections[2].z=900; + assert(!native_wide_projection_x(addr,word,1023,151,&px)); + fixture_projections[2].x16=4096*65536-1; + assert(!native_wide_projection_x(addr,word,1023,152,&px)); + fixture_projections[2].x16=1000*65536; + assert(!native_wide_projection_x(addr,word,1023,152,&px)); + /* Negative saturation is recovered only when the shadow extends left. */ + fixture_projections[2].word=pack_vertex(-1024,152); + fixture_projections[2].x16=-1200*65536; + assert(native_wide_projection_x(addr,fixture_projections[2].word,-1024,152,&px)); + fixture_projections[2].x16=-4096*65536; + assert(!native_wide_projection_x(addr,fixture_projections[2].word,-1024,152,&px)); + fixture_projections[2].x16=1040*65536; + fixture_projections[2].word=word; + ws_mode=0; + prepare_precise_triangle(7,4,10,bx,by); + assert(!fixture_precise_triangle.enabled); /* 4:3 remains stock */ + ws_mode=2; ws_cfg_num=4; ws_cfg_den=3; + prepare_precise_triangle(7,4,10,bx,by); + assert(!fixture_precise_triangle.enabled); /* native-wide selected, zero reveal */ + configure_native_wide_16_9(); + psx_mod_set_native_wide_projection_correction(0); + assert(!fixture_pgxp_enabled); + prepare_precise_triangle(7,4,10,bx,by); + assert(!fixture_precise_triangle.enabled); + puts("native_wide_projection_test: PASS"); + return 0; +} diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp index 2b022f24..b963717c 100644 --- a/runtime/tests/test_pgxp.cpp +++ b/runtime/tests/test_pgxp.cpp @@ -274,6 +274,21 @@ int main(void) { CHECK(!valid); } + /* Saturated SXY retains the original projection in exact transport. The + * ordinary precision consumer stays conservative; the opt-in native-wide + * renderer can query the address/word-validated projection separately. */ + { + const uint32_t packed = (152u << 16) | 1023u; + pgxp_gte_push_sxy(1040 * 65536, 152 * 65536 + 17000, 994, packed); + psx_pgxp_cop2(nullptr, MFC2(9, 14), packed, 0); + psx_pgxp_store(nullptr, SW(1, 9), ADDR_B, packed); + int32_t x, y; uint16_t z; + CHECK(pgxp_load_precise_word(ADDR_B, packed, &x, &y, &z)); + CHECK(x == 1040 * 65536 && y == 152 * 65536 + 17000 && z == 994); + CHECK(!pgxp_load_precise_word(ADDR_B, packed ^ 1, &x, &y, &z)); + CHECK(lookup(ADDR_B, packed, 1023, 152, &x, &y, &z) == PGXP_SRC_NATIVE); + } + /* --- stats sanity: dataflow hits were counted --- */ { PGXPStats st;