Recover saturated horizontal projections in opt-in native-wide rendering

This commit is contained in:
Matthew Stanley
2026-10-02 16:00:02 -07:00
parent c29e934308
commit 3787120488
9 changed files with 228 additions and 9 deletions
+35
View File
@@ -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.
+4 -1
View File
@@ -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)
+1
View File
@@ -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. */
+5
View File
@@ -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.
+9 -2
View File
@@ -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>`.
+47 -3
View File
@@ -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);
}
+21 -3
View File
@@ -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;
@@ -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;
}
+15
View File
@@ -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;