mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Recover saturated horizontal projections in opt-in native-wide rendering
This commit is contained in:
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user