From cd9981cda8ec7c327fdb563e05ca3559d1950711 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 3 Oct 2026 23:42:23 -0700 Subject: [PATCH] Clip proven native-wide world faces across camera and screen planes --- docs/ENHANCEMENTS.md | 26 ++++ runtime/include/cpu_state.h | 1 + runtime/include/gpu_gl_renderer.h | 11 ++ runtime/include/gpu_projective_clip.h | 52 ++++++++ runtime/include/mod_plugins.h | 10 ++ runtime/include/pgxp.h | 10 ++ runtime/src/debug_server.c | 6 +- runtime/src/gpu.c | 134 ++++++++++++++++++-- runtime/src/gpu_gl_renderer.c | 56 +++++++- runtime/src/gte.cpp | 41 ++++++ runtime/src/pgxp.cpp | 37 +++++- runtime/tests/gpu_exec_stubs.inc | 31 ++++- runtime/tests/test_gl_readback_region.c | 94 ++++++++++++++ runtime/tests/test_gte_register_access.cpp | 39 +++++- runtime/tests/test_native_wide_projection.c | 77 +++++++++++ runtime/tests/test_pgxp.cpp | 27 ++++ runtime/tests/test_ws_cull_edge_interp.c | 20 +++ 17 files changed, 649 insertions(+), 23 deletions(-) create mode 100644 runtime/include/gpu_projective_clip.h diff --git a/docs/ENHANCEMENTS.md b/docs/ENHANCEMENTS.md index 88fabe24..2c3c9701 100644 --- a/docs/ENHANCEMENTS.md +++ b/docs/ENHANCEMENTS.md @@ -1280,3 +1280,29 @@ identity when off; main executable only (overlay code keeps its own code); native code and the dirty-RAM interpreter agree (`draw_distance_codegen_test`, `draw_distance_interp_test`). First user: R4's course renderers (RidgeRacerType4Recomp, Max Detail). + +### Opt-in camera-plane clipping for native-wide textured worlds + +`psx_mod_set_native_wide_near_clip(1)` complements horizontal projection +recovery. A large face can remain visible while a corner crosses the camera +plane: unsigned SZ, the capped H/Z divider and saturated SXY cannot represent +that face. PGXP now optionally carries the signed homogeneous projection from +RTPS/RTPT through exact full-word copies. CPU arithmetic, partial writes, +stale words and timeline invalidation discard the association; sandbox +rollback restores it with the rest of the precision shadow. + +Opted-in native-wide OpenGL sessions clip proven textured faces at depth 1 +and the visible bounds before perspective division. UV and color attributes +follow each intersection, and ordinary GPU batches, painter order, masking, +texture filtering and canonical/wide readback remain in use. The existing +guest GTE registers, gameplay state and title culling branches do not change. +Software/CPU-authoritative rendering, 4:3 and missing provenance use the +original path. The larger PGXP value increases the full 8 MiB RAM shadow +from 40 to 72 MiB; projection calculations are disabled unless a title opts in. + +MediEvil II's title hallway provided the camera-crossing reproduction. The +GL authority/order fixture passes 197 checks at 1x and 4x; provenance tests +cover negative depth, stale packets, identical partial writes, rollback and +mode guards. Live captures remove the previously observed triangular holes; +the owner confirmed the hallway walls stay intact. Broader map coverage +remains separate from this reproduction. diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 8c6665f7..8b1c8fd5 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -240,6 +240,7 @@ extern void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks, * native comparison. This does not change guest-visible GTE state. */ extern int gte_nclip_precise_bltz(int32_t native_mac0); extern int gte_nclip_native_wide_sign(int32_t native_mac0, int* sign); +extern int gte_nclip_native_wide_previous_sign(int32_t native_mac0, int* sign); extern int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla); /* PGXP dataflow-shadowing hook macros (PGXP_LOAD/STORE/ALU/MULDIV/COP2). diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index fa291473..9f6dd817 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -8,6 +8,7 @@ * same struct tag) so this header needs no SDL include. */ #include +#include "gpu_projective_clip.h" struct SDL_Window; @@ -331,6 +332,16 @@ int gl_renderer_get_ws_ablate(void); /* Cumulative textured fraction of scene primitives since boot (flat vs textured * batching decision). Sets *out_tex_frac; returns total prim count. */ uint64_t gl_renderer_perf_prim_split(double *out_tex_frac); +/* Counts proven projected world triangles admitted past the hardware size + * reject. Diagnostic only; they use the normal canonical and mirror passes. */ +void gl_renderer_note_wide_triangle_recovery(int enabled); +uint64_t gl_renderer_wide_triangle_recovery_count(void); +/* Exact-provenance world polygons only. Returns zero when the GPU context or + * authority policy cannot support clipping; the caller then uses its stock path. */ +int gl_renderer_projective_supported(void); +void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3], + uint16_t texpage, uint16_t clut_x, uint16_t clut_y, int raw, int semi, + int perspective); /* Cumulative textured-batch diagnostics: total, then flushes caused by * isolation, blend-mode, mask, filter, backdrop-gate, texture-window, capacity. */ void gl_renderer_batch_diag(uint64_t out[8]); diff --git a/runtime/include/gpu_projective_clip.h b/runtime/include/gpu_projective_clip.h new file mode 100644 index 00000000..28d6c8c4 --- /dev/null +++ b/runtime/include/gpu_projective_clip.h @@ -0,0 +1,52 @@ +#ifndef PSX_GPU_PROJECTIVE_CLIP_H +#define PSX_GPU_PROJECTIVE_CLIP_H + +/* Clip before dividing by signed camera depth. Attributes follow the same + * edge parameter, so clipped triangles retain their original texture plane. */ +typedef struct PSXProjectedVertex { + double x, y, z, u, v, r, g, b; +} PSXProjectedVertex; + +static inline PSXProjectedVertex psx_projective_lerp( + PSXProjectedVertex a, PSXProjectedVertex b, double t) { + PSXProjectedVertex p; +#define LERP(f) p.f = a.f + (b.f-a.f)*t + LERP(x); LERP(y); LERP(z); LERP(u); LERP(v); + LERP(r); LERP(g); LERP(b); +#undef LERP + return p; +} +static inline double psx_projective_distance(PSXProjectedVertex p, int plane, + double left, double right, double top, double bottom) { + switch (plane) { + case 0: return p.z - 1.0; + case 1: return p.x - left*p.z; + case 2: return right*p.z - p.x; + case 3: return p.y - top*p.z; + default: return bottom*p.z - p.y; + } +} +static inline int psx_projective_clip_triangle(const PSXProjectedVertex in[3], + PSXProjectedVertex out[12], double left, double right, double top, double bottom) { + PSXProjectedVertex a[12], b[12]; + int n=3; + for (int i=0;i<3;++i) a[i]=in[i]; + for (int plane=0;plane<5 && n;++plane) { + int m=0; + PSXProjectedVertex prev=a[n-1]; + double dp=psx_projective_distance(prev,plane,left,right,top,bottom); + for (int i=0;i=0)!=(dc>=0)) + b[m++]=psx_projective_lerp(prev,cur,dp/(dp-dc)); + if (dc>=0) b[m++]=cur; + prev=cur; dp=dc; + } + n=m; + for (int i=0;i pct=<0..100>`. diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index c1dc3932..baec4a62 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -897,6 +897,7 @@ int psx_ws_masked_reject_site(uint32_t pc, uint32_t instr, } static WsMaskedRejectSite ws_nclip_sites[WS_EXPLICIT_CULL_SITES_MAX]; +static uint8_t ws_nclip_previous[WS_EXPLICIT_CULL_SITES_MAX]; static int ws_nclip_site_count; static uint64_t ws_nclip_rescues; uint64_t gpu_ws_native_wide_nclip_rescues(void) { return ws_nclip_rescues; } @@ -908,18 +909,33 @@ void psx_mod_set_native_wide_nclip_sites(const uint32_t* addresses, if (!((op == 1 && rt <= 1) || ((op == 6 || op == 7) && rt == 0))) abort(); ws_nclip_sites[i].address = addresses[i] & 0x1FFFFFFFu; ws_nclip_sites[i].expected = expected[i]; + ws_nclip_previous[i] = 0; } ws_nclip_site_count = count; ws_nclip_rescues = 0; } +void psx_mod_set_native_wide_nclip_previous_site(uint32_t address, uint32_t expected) { + for (int i = 0; i < ws_nclip_site_count; ++i) { + if (ws_nclip_sites[i].address == (address & 0x1FFFFFFFu) && + ws_nclip_sites[i].expected == expected) { + ws_nclip_previous[i] = 1; + return; + } + } + abort(); +} extern int gte_nclip_native_wide_sign(int32_t mac0, int* sign); +extern int gte_nclip_native_wide_previous_sign(int32_t mac0, int* sign); int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla) { if (psx_ws_x_margin() <= 0 || !gpu_ws_precise_nclip_enabled()) return vanilla; for (int i = 0; i < ws_nclip_site_count; ++i) { if (ws_nclip_sites[i].address != (pc & 0x1FFFFFFFu) || ws_nclip_sites[i].expected != instr) continue; int sign; - if (!gte_nclip_native_wide_sign(mac0, &sign)) return vanilla; + const int valid = ws_nclip_previous[i] ? + gte_nclip_native_wide_previous_sign(mac0, &sign) : + gte_nclip_native_wide_sign(mac0, &sign); + if (!valid) return vanilla; const uint32_t op = instr >> 26; const int result = op == 6 ? sign <= 0 : op == 7 ? sign > 0 : ((instr >> 16) & 1) ? sign >= 0 : sign < 0; @@ -4039,6 +4055,15 @@ int gpu_ws_native_wide_projection_correction(uint64_t *vertices) { return s_native_wide_projection_correction; } +static int gpu_ws_projective_enabled(void) { + return pgxp_projection_tracking() && s_native_wide_projection_correction && ws_native_wide_active() && + ws_nw_extra() > 0 && gr_backend() == GR_BACKEND_OPENGL && + gl_renderer_projective_supported(); +} +void psx_mod_set_native_wide_near_clip(int enabled) { + pgxp_set_projection_tracking(enabled); +} + /* 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 @@ -4064,6 +4089,38 @@ static int native_wide_projection_x(uint32_t addr, uint32_t word, return 1; } +/* Original, unsubdivided world faces can exceed the PS1 size limit after the + * wider view admits them. Proven projected triangles use normal GL clipping, + * with the SAME geometry in canonical and wide passes: margin-only recovery + * splits a face at the presentation center-copy edge. Missing/stale provenance, + * depth behind the camera, saturated Y and UI retain the hardware reject. */ +static int gpu_triangle_rejected(const int32_t* x, const int32_t* y, + int a, int b, int c) { + if (!psx_gpu_triangle_oversize(x, y, a, b, c)) return 0; + if (!s_native_wide_projection_correction || !ws_native_wide_active() || + ws_nw_extra() <= 0 || gr_backend() != GR_BACKEND_OPENGL || + gp0_cmd_source_addr == UINT32_MAX) return 1; + const uint32_t op = gp0_cmd_buf[0] >> 24; + const unsigned stride = 1u + ((op & 4u) != 0) + ((op & 16u) != 0); + const int vertices[3] = {a, b, c}; + for (unsigned i = 0; i < 3; ++i) { + const unsigned index = 1u + stride * (unsigned)vertices[i]; + const uint32_t word = gp0_cmd_buf[index]; + int32_t px, py, rx, ry; + uint16_t z; + parse_vertex(word, &rx, &ry); + if (!gte_precision_load_word(gp0_cmd_source_addr + 4u * index, + word, &px, &py, &z) || !z || + (py >> 16) != ry || ry == -1024 || ry == 1023 || + px <= -4096 * 65536 || + px >= 4096 * 65536 - 1) return 1; + const int exact_x = px >> 16; + if (rx == -1024 ? exact_x > -1024 : + rx == 1023 ? exact_x < 1023 : exact_x != rx) return 1; + } + return 0; +} + void gpu_texture_correction_set(int enabled) { s_texture_correction_enabled = enabled ? 1 : 0; gpu_pgxp_rederive_enable(); @@ -4091,6 +4148,15 @@ uint32_t gpu_texture_correction_hits(void) { * caller. */ static void prepare_precise_triangle(int i0, int i1, int i2, const int32_t vx[3], const int32_t vy[3]) { + if (gr_backend() == GR_BACKEND_OPENGL) { + const int indices[3] = {i0,i1,i2}; + int32_t raw_x[3], raw_y[3]; + for (unsigned i=0; i<3; ++i) + parse_vertex(gp0_cmd_buf[indices[i]], &raw_x[i], &raw_y[i]); + gl_renderer_note_wide_triangle_recovery(s_native_wide_projection_correction && + ws_native_wide_active() && + psx_gpu_triangle_oversize(raw_x,raw_y,0,1,2)); + } gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f); const int geometry = gte_geometry_correction_enabled(); if (!geometry && !s_native_wide_projection_correction) { @@ -4395,7 +4461,7 @@ static void gp0_exec_mono_tri(void) { for (int i = 0; i < 3; i++) { parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]); } - if (psx_gpu_triangle_oversize(vx, vy, 0, 1, 2)) return; + if (gpu_triangle_rejected(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { vx[i] += draw_offset_x; @@ -4415,8 +4481,8 @@ static void gp0_exec_mono_quad(void) { int32_t vx[4], vy[4]; for (int i = 0; i < 4; i++) parse_vertex(gp0_cmd_buf[1 + i], &vx[i], &vy[i]); - int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2); - int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); + int rej_a = gpu_triangle_rejected(vx, vy, 0, 1, 2); + int rej_b = gpu_triangle_rejected(vx, vy, 2, 1, 3); if (rej_a && rej_b) return; int radial_mask=ws_nw_radial_mask_transform(vx,vy); int screen_mask=ws_native_wide_active() && gp0_cmd_source_addr != UINT32_MAX && @@ -4513,7 +4579,7 @@ static void gp0_exec_shaded_tri(void) { c[i] = rgb888_to_rgb555(gp0_cmd_buf[i * 2] & 0xFFFFFFu); parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]); } - if (psx_gpu_triangle_oversize(vx, vy, 0, 1, 2)) return; + if (gpu_triangle_rejected(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { vx[i] += draw_offset_x; @@ -4545,8 +4611,8 @@ static void gp0_exec_shaded_quad(void) { c[i] = rgb888_to_rgb555(gp0_cmd_buf[i * 2] & 0xFFFFFFu); parse_vertex(gp0_cmd_buf[1 + i * 2], &vx[i], &vy[i]); } - int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2); - int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); + int rej_a = gpu_triangle_rejected(vx, vy, 0, 1, 2); + int rej_b = gpu_triangle_rejected(vx, vy, 2, 1, 3); if (rej_a && rej_b) return; int radial_mask=ws_nw_radial_mask_transform(vx,vy); if (!radial_mask) { @@ -4619,8 +4685,47 @@ static void setup_textured_draw(uint32_t color24, int semi_trans, int raw_textur gr_set_semi_transparency(semi_trans, (int)semi_transparency); } +/* A close corner may cross the camera plane while the face is still visible. + * The hardware's clamped H/Z and unsigned SZ cannot describe that polygon. + * Use only intact GTE-derived words, clip their signed homogeneous projection, + * and submit through the normal ordered GL passes and VRAM authority path. */ +static int native_wide_projective_draw(void) { + if (!gpu_ws_projective_enabled() || gp0_cmd_source_addr == UINT32_MAX) return 0; + const unsigned op=gp0_cmd_buf[0]>>24; + const unsigned stride=(op&16u)?3u:2u, n=(op&8u)?4u:3u; + PSXProjectedVertex vertices[4]; + int needs_clip=0; + for (unsigned i=0;i>8)&255, + (color&255)/255.0,((color>>8)&255)/255.0,((color>>16)&255)/255.0}; + } + if (!needs_clip) return 0; + const uint16_t clut=gp0_cmd_buf[2]>>16; + const uint16_t tpage=gp0_cmd_buf[2+stride]>>16; + set_tpage_from_poly(tpage); + const int semi=(op&2u)?(int)semi_transparency:-1; + gl_renderer_draw_projected_triangle(vertices,tpage&0x1ff, + (clut&63)*16,(clut>>6)&511,op&1u,semi,s_texture_correction_enabled); + if (n==4) { + const PSXProjectedVertex second[3]={vertices[2],vertices[1],vertices[3]}; + gl_renderer_draw_projected_triangle(second,tpage&0x1ff, + (clut&63)*16,(clut>>6)&511,op&1u,semi,s_texture_correction_enabled); + } + return 1; +} + /* Execute textured triangle (GP0 0x24-0x27) */ static void gp0_exec_textured_tri(void) { + if (native_wide_projective_draw()) return; uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu; int semi_trans = (gp0_cmd_buf[0] >> 25) & 1; int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; @@ -4640,7 +4745,7 @@ static void gp0_exec_textured_tri(void) { uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16); uint16_t tpage = tpage_word & 0x1FF; set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ - if (psx_gpu_triangle_oversize(vx, vy, 0, 1, 2)) return; + if (gpu_triangle_rejected(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { @@ -4661,6 +4766,7 @@ static void gp0_exec_textured_tri(void) { /* Execute textured quad (GP0 0x2C-0x2F) */ static void gp0_exec_textured_quad(void) { + if (native_wide_projective_draw()) return; uint32_t color24 = gp0_cmd_buf[0] & 0xFFFFFFu; int semi_trans = (gp0_cmd_buf[0] >> 25) & 1; int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; @@ -4681,8 +4787,8 @@ static void gp0_exec_textured_quad(void) { uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[4] >> 16); uint16_t tpage = tpage_word & 0x1FF; set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ - int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2); - int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); + int rej_a = gpu_triangle_rejected(vx, vy, 0, 1, 2); + int rej_b = gpu_triangle_rejected(vx, vy, 2, 1, 3); if (rej_a && rej_b) return; /* Widescreen: tagged billboard quads carry CPU-computed pixel offsets the @@ -4764,6 +4870,7 @@ static void gp0_exec_textured_quad(void) { /* Execute shaded textured triangle (GP0 0x34-0x37) */ static void gp0_exec_shaded_textured_tri(void) { + if (native_wide_projective_draw()) return; int semi_trans = (gp0_cmd_buf[0] >> 25) & 1; int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; int32_t vx[3], vy[3]; @@ -4785,7 +4892,7 @@ static void gp0_exec_shaded_textured_tri(void) { uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[5] >> 16); uint16_t tpage = tpage_word & 0x1FF; set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ - if (psx_gpu_triangle_oversize(vx, vy, 0, 1, 2)) return; + if (gpu_triangle_rejected(vx, vy, 0, 1, 2)) return; ws_nw_hud_shift_vertices(vx, 3); for (int i = 0; i < 3; i++) { @@ -4828,6 +4935,7 @@ int psx_mod_texture_banks_supported(void) { /* Execute shaded textured quad (GP0 0x3C-0x3F) */ static void gp0_exec_shaded_textured_quad(void) { + if (native_wide_projective_draw()) return; int semi_trans = (gp0_cmd_buf[0] >> 25) & 1; int raw_texture = (gp0_cmd_buf[0] >> 24) & 1; int32_t vx[4], vy[4]; @@ -4852,8 +4960,8 @@ static void gp0_exec_shaded_textured_quad(void) { uint16_t tpage_word = (uint16_t)(gp0_cmd_buf[5] >> 16); uint16_t tpage = tpage_word & 0x1FF; set_tpage_from_poly(tpage_word); /* latches even for size-rejected polys */ - int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2); - int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3); + int rej_a = gpu_triangle_rejected(vx, vy, 0, 1, 2); + int rej_b = gpu_triangle_rejected(vx, vy, 2, 1, 3); if (rej_a && rej_b) return; ws_auto_ui_transform_quad(vx, vy); diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 930d12b9..c0192716 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -555,6 +555,8 @@ static GLuint s_pack_prog = 0, s_stencil_prog = 0, s_empty_vao = 0; * gpu.c sets these immediately before the matching gr_draw_*_triangle and they * are consumed by it. All-zero == the faithful integer/affine path. */ static int s_pc_valid = 0; /* sub-pixel positions present */ +static int s_projected_uv_valid; +static float s_projected_u[3], s_projected_v[3]; static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */ static int s_pq_valid = 0; /* perspective weights present */ static float s_pq[3]; @@ -697,6 +699,16 @@ static int g_wide_cur_base = 0; /* base_x of g_wide_cur */ * gpu_geometry wide mirror is skipped and the flat path emits its own * full-wide-width pass instead (mirrors sw_draw_flat_rect). */ static int s_wide_suppress = 0; +/* Diagnostic only. The frontend admits proven world geometry into both the + * canonical and wide passes so a face cannot split at the center-copy edge. */ +static uint64_t s_wide_triangle_recovery_count; +void gl_renderer_note_wide_triangle_recovery(int recovered) { + if (recovered && s_raster_ok && g_wide_cur) ++s_wide_triangle_recovery_count; +} +uint64_t gl_renderer_wide_triangle_recovery_count(void) { + return s_wide_triangle_recovery_count; +} + /* X-translation (native px) from canonical VRAM space into the active wide * surface: local_x = vram_x - base_x + OFFSET. Same as SW wide_dx(). */ @@ -3295,7 +3307,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys, int semi, const int *lim) { // Mode 2 needs proven world geometry. Sprites, HUD and untracked packets // retain point sampling; their cutout pixels must stay sharp. - const int filter=s_tex_filter==2 && (lim || (!s_pc_valid && !s_pq_valid)) ? 0 : s_tex_filter; + const int filter=s_tex_filter==2 && ((lim && !s_projected_uv_valid) || (!s_pc_valid && !s_pq_valid)) ? 0 : s_tex_filter; int lim_buf[4]; int uv_buf[6]; if (!lim) { @@ -3386,7 +3398,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys, for (int i = 0; i < 3; i++, vp += TEXV) { vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i]; vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i]; - vp[2] = (float)us[i]; vp[3] = (float)vs[i]; + vp[2] = s_projected_uv_valid ? s_projected_u[i] : (float)us[i]; + vp[3] = s_projected_uv_valid ? s_projected_v[i] : (float)vs[i]; vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f; vp[8] = (float)base_x; vp[9] = (float)base_y; /* a_tpage */ vp[10] = (float)clut_x; vp[11] = (float)clut_y; /* a_clut */ @@ -3704,7 +3717,44 @@ static int cpu_raster_required(void) { } /* The sub-pixel / perspective override describes exactly one triangle; drop it * once that triangle has been submitted so a later prim can never inherit it. */ -static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; } +static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; s_projected_uv_valid = 0; } + +int gl_renderer_projective_supported(void) { + return s_raster_ok && g_wide_cur && !cpu_vram_authoritative(); +} +void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3], + uint16_t texpage, uint16_t cx, uint16_t cy, int raw, int semi, int perspective) { + PSXProjectedVertex polygon[12]; + const double left=fmin(s_area_x1, -wide_dx())-1.0; + const double right=fmax(s_area_x2+1, g_wide_w-wide_dx())+1.0; + const int n=psx_projective_clip_triangle(vertices,polygon, + left,right,s_area_y1-1.0,s_area_y2+1.0); + int limits[4]={(int)vertices[0].u,(int)vertices[0].v, + (int)vertices[0].u,(int)vertices[0].v}; + for (int i=1;i<3;++i) { + if (vertices[i].ulimits[2]) limits[2]=(int)vertices[i].u; + if (vertices[i].v>limits[3]) limits[3]=(int)vertices[i].v; + } + for (int k=1;k+1SZ[3], (uint32_t)gte->SXY[2]); + if (pgxp_projection_tracking() && shift == 12 && !lm) { + const double z = (double)(mac3 >> 12); + double hx = (double)gte->MAC1 * gte->H; + if (do_squash) hx = hx * s_ws_xnum / s_ws_xden; + else if (s_ws_dome_on && s_ws_dome_num != s_ws_dome_den && + !gpu_ws_present_native_43() && gte->SZ[3] >= s_ws_far_threshold) + hx = hx * s_ws_dome_num / s_ws_dome_den; + PGXPProjection projection = { + (float)(gte->OFX / 65536.0 * z + hx), + (float)(gte->OFY / 65536.0 * z + (double)gte->MAC2 * gte->H), + (float)z, (float)gte->H / 2.0f}; + pgxp_gte_set_projection(&projection); + } } geom_note((uint32_t)gte->SXY[2], sx16, sy16); @@ -1045,6 +1060,15 @@ static int8_t s_nclip_last_precise_sign = 0; static bool s_nclip_last_precise_valid = false; static bool s_nclip_last_horizontal_saturated = false; static uint32_t s_nclip_last_generation; +static int32_t s_nclip_previous_native; +static int8_t s_nclip_previous_precise_sign; +static bool s_nclip_previous_precise_valid; +static bool s_nclip_previous_horizontal_saturated; +static uint32_t s_nclip_previous_generation; +static void gte_nclip_precision_invalidate() { + s_nclip_last_precise_valid = false; + s_nclip_previous_precise_valid = false; +} extern "C" void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks, uint64_t *disagreements) { if (hits) *hits = s_nclip_precise_hits; @@ -1063,6 +1087,17 @@ extern "C" int gte_nclip_native_wide_sign(int32_t native_mac0, int* sign) { *sign = s_nclip_last_precise_sign; return 1; } +/* Some guarded quad consumers save two MAC0 values before branching. Their + * first branch must use the first command's provenance, even when both native + * results are equal. This is explicitly selected by the title, never searched + * as a fallback for arbitrary architectural MAC0 readers. */ +extern "C" int gte_nclip_native_wide_previous_sign(int32_t native_mac0, int* sign) { + if (!s_nclip_previous_precise_valid || !s_nclip_previous_horizontal_saturated || + native_mac0 != s_nclip_previous_native || s_nclip_previous_generation != s_geom_generation || + s_gte_replay_sandbox || s_speculative_depth != 0) return 0; + *sign = s_nclip_previous_precise_sign; + return 1; +} void gte_nclip(GTEState* gte, uint32_t instr) { gte->FLAG = 0; @@ -1108,6 +1143,12 @@ void gte_nclip(GTEState* gte, uint32_t instr) { } } } + s_nclip_previous_native = s_nclip_last_native; + s_nclip_previous_precise_sign = s_nclip_last_precise_sign; + s_nclip_previous_precise_valid = s_nclip_last_precise_valid && + !s_gte_replay_sandbox && s_speculative_depth == 0; + s_nclip_previous_horizontal_saturated = s_nclip_last_horizontal_saturated; + s_nclip_previous_generation = s_nclip_last_generation; s_nclip_last_native = out; s_nclip_last_precise_valid = false; s_nclip_last_horizontal_saturated = false; diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index 0c601fa1..5471d14d 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -52,6 +52,7 @@ enum { PGXP_F_VX = 1u << 0, /* low half (screen X) tracked */ PGXP_F_VY = 1u << 1, /* high half (screen Y) tracked */ PGXP_F_VZ = 1u << 2, /* projected depth rode along intact */ + PGXP_F_PROJECTION = 1u << 3, PGXP_F_VXY = PGXP_F_VX | PGXP_F_VY, }; @@ -61,6 +62,7 @@ struct PGXPValue { uint16_t flags; uint32_t value; /* the guest word this shadow describes */ uint32_t gen; /* valid iff == s_gen (O(1) invalidate-all) */ + PGXPProjection projection; }; /* Shadow covers the host RAM backing so the opt-in 8 MB map tracks its high @@ -70,7 +72,7 @@ struct PGXPValue { #define PGXP_REG_HI 32 #define PGXP_REG_LO 33 -static PGXPValue *s_ram = nullptr; /* lazily allocated, ~40 MB VA */ +static PGXPValue *s_ram = nullptr; /* lazily allocated, 72 MiB VA */ static PGXPValue s_scratch[PGXP_SCRATCH_WORDS]; static PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */ static PGXPValue s_gte[32]; /* GTE data registers */ @@ -78,6 +80,7 @@ static PGXPValue s_gte[32]; /* GTE data registers */ static uint32_t s_gen = 1; static int s_enabled = 0; static int s_cpu_mode = 0; +static int s_projection_tracking = 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) */ @@ -135,6 +138,13 @@ extern "C" int pgxp_active(void) { return g_pgxp_active; } extern "C" void pgxp_set_cpu_mode(int enabled) { s_cpu_mode = enabled ? 1 : 0; } extern "C" int pgxp_cpu_mode(void) { return s_cpu_mode; } +extern "C" void pgxp_set_projection_tracking(int enabled) { + if (s_projection_tracking == !!enabled) return; + s_projection_tracking = !!enabled; + pgxp_invalidate_all(); +} +extern "C" int pgxp_projection_tracking(void) { return s_projection_tracking; } + extern "C" void pgxp_set_tolerance(float pixels) { s_tolerance = pixels; } extern "C" float pgxp_tolerance(void) { return s_tolerance; } @@ -340,6 +350,7 @@ static inline int pv_live(const PGXPValue *pv) { static inline void pv_validate(PGXPValue *pv, uint32_t actual) { if (pv->gen != s_gen) return; uint32_t diff = pv->value ^ actual; + if (diff) pv->flags &= (uint16_t)~PGXP_F_PROJECTION; if ((pv->flags & PGXP_F_VX) && (diff & 0x0000FFFFu)) pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_VZ); if ((pv->flags & PGXP_F_VY) && (diff & 0xFFFF0000u)) @@ -448,7 +459,7 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, * half-write — the vertex it described no longer exists whole. */ if (hi_half) dst->value = (dst->value & 0x0000FFFFu) | (half << 16); else dst->value = (dst->value & 0xFFFF0000u) | half; - dst->flags &= (uint16_t)~((hi_half ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ); + dst->flags &= (uint16_t)~((hi_half ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ | PGXP_F_PROJECTION); dst->z = 0; if (src && src->gen == s_gen && (src->flags & PGXP_F_VX) && ((src->value ^ value) & 0xFFFFu) == 0) { @@ -462,7 +473,7 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, uint32_t shift = (addr & 3u) * 8u; dst->value = (dst->value & ~(0xFFu << shift)) | ((value & 0xFFu) << shift); - dst->flags &= (uint16_t)~(((addr & 2u) ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ); + dst->flags &= (uint16_t)~(((addr & 2u) ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ | PGXP_F_PROJECTION); dst->z = 0; return; } @@ -874,6 +885,26 @@ extern "C" int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16) { return pgxp_get_gte_sxy_checked(index, 0u, 0, x16, y16); } +extern "C" void pgxp_gte_set_projection(const PGXPProjection *projection) { + if (!g_pgxp_active || !s_projection_tracking || !projection) return; + s_gte[14].projection = *projection; + s_gte[14].flags |= PGXP_F_PROJECTION; + s_gte[15] = s_gte[14]; +} +static int projection_read(const PGXPValue *pv, uint32_t packed, PGXPProjection *out) { + const uint16_t flags = PGXP_F_VXY | PGXP_F_PROJECTION; + if (!s_enabled || !s_projection_tracking || !pv || pv->gen != s_gen || pv->value != packed || + (pv->flags & flags) != flags) return 0; + if (out) *out = pv->projection; + return 1; +} +extern "C" int pgxp_load_projection(uint32_t addr, uint32_t packed, PGXPProjection *out) { + return projection_read(pgxp_ptr(addr), packed, out); +} +extern "C" int pgxp_get_gte_projection(uint32_t index, uint32_t packed, PGXPProjection *out) { + return index < 4 ? projection_read(&s_gte[12+index], packed, out) : 0; +} + extern "C" int pgxp_get_gte_sxy_checked(uint32_t index, uint32_t expect, int check, int32_t *x16, int32_t *y16) { if (index >= 4) return 0; diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index 48345249..c9224e95 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -16,8 +16,30 @@ uint32_t g_psx_ram_mask = 0x001FFFFFu; /* This fixture exercises stock GPU packets without a GL context or mod bank. * Keep the gpu.c dependencies explicit, including on linkers that retain * additional exported command handlers despite --gc-sections. */ -GrBackend gr_backend(void) { return GR_BACKEND_SOFTWARE; } +static GrBackend fixture_backend = GR_BACKEND_SOFTWARE; +GrBackend gr_backend(void) { return fixture_backend; } int gl_renderer_texture_banks_supported(void) { return 0; } +static int fixture_projective_supported, fixture_projective_draws; +static PSXProjectedVertex fixture_projective_vertices[3]; +int gl_renderer_projective_supported(void) { return fixture_projective_supported; } +void gl_renderer_draw_projected_triangle(const PSXProjectedVertex v[3], + uint16_t tp, uint16_t cx, uint16_t cy, int raw, int semi, int perspective) { + memcpy(fixture_projective_vertices,v,sizeof fixture_projective_vertices); + fixture_projective_draws++; + (void)tp;(void)cx;(void)cy;(void)raw;(void)semi;(void)perspective; +} +static struct { uint32_t addr,word; PGXPProjection p; int valid; } fixture_homogeneous[4]; +int pgxp_load_projection(uint32_t addr, uint32_t word, PGXPProjection *out) { + for(int i=0;i<4;++i) if(fixture_homogeneous[i].valid && + fixture_homogeneous[i].addr==addr && fixture_homogeneous[i].word==word) { + if(out)*out=fixture_homogeneous[i].p; + return 1; + } + return 0; +} +static int fixture_projection_tracking; +int pgxp_projection_tracking(void) { return fixture_projection_tracking; } +void pgxp_set_projection_tracking(int enabled) { fixture_projection_tracking=!!enabled; } int gl_renderer_select_texture_bank(uint16_t id) { return id == 0; } int gl_renderer_select_texture_bank_live_clut(uint16_t id) { return id == 0; } uint16_t mod_texture_packet_bank(uint32_t source, const uint32_t *words, @@ -376,3 +398,10 @@ int gr_render_wide_display(uint32_t *out, int pitch, int base_x, int gte_nclip_native_wide_sign(int32_t mac0, int* sign) { (void)mac0; (void)sign; return 0; } +int gte_nclip_native_wide_previous_sign(int32_t mac0, int* sign) { + (void)mac0; (void)sign; return 0; +} +static int fixture_triangle_recovered; +void gl_renderer_note_wide_triangle_recovery(int enabled) { + fixture_triangle_recovered=enabled; +} diff --git a/runtime/tests/test_gl_readback_region.c b/runtime/tests/test_gl_readback_region.c index 2f9be518..44e46761 100644 --- a/runtime/tests/test_gl_readback_region.c +++ b/runtime/tests/test_gl_readback_region.c @@ -61,6 +61,98 @@ static void verify_bank_batching(void) { psx_mod_set_texture_bank_batching(0);s_tex_filter=0; glb_set_mask_bits(0,0);glb_set_semi_transparency(0,0); } +static void verify_oversize_wide_geometry(int scale) { + /* Captured hallway triangles exceed the PS1 height limit. Proven geometry + * must use identical canonical/wide passes, including the center-copy edge. */ + glb_set_draw_area(0,0,511,239);glb_set_precise_triangle(0,0,0,0,0,0,0); + glb_wide_configure(848,168);glb_wide_set_target(0); + glb_wide_set_view(0,0,0,0);s_wide_fast=1; + glb_wide_clear(0,0,240,0); + glb_draw_flat_rect(-168,0,848,240,0x7c00); + gl_renderer_note_wide_triangle_recovery(1); + glb_draw_flat_triangle(-300,-200,300,100,-100,500,0x001f); + check(glb_vram_read(100,100)==0x001f,"recovered flat updates canonical pixels coherently"); + /* A line queued immediately after a recovered flat triangle must use + * the ordinary batch at both native and supersampled resolutions. */ + gl_renderer_note_wide_triangle_recovery(1); + glb_draw_flat_triangle(-300,-200,300,100,-100,500,0x001f); + glb_draw_line(203,40,219,40,0x03e0); + check(glb_vram_read(210,40)==0x03e0,"ordinary line after oversize flat"); + glb_vram_write(512,0,0x03e0); + gl_renderer_note_wide_triangle_recovery(1); + glb_draw_shaded_textured_triangle(561,184,0,0,0x808080, + 1023,-724,0,0,0x808080,1023,334,0,0,0x808080,0,0,0x108,1); + gl_renderer_note_wide_triangle_recovery(1); + glb_draw_shaded_textured_triangle(-300,-200,0,0,0x808080, + 300,100,0,0,0x808080,-100,500,0,0,0x808080,0,0,0x108,1); + check(glb_vram_read(100,100)==0x03e0,"recovered textured updates canonical pixels coherently"); + gl_renderer_note_wide_triangle_recovery(1); + glb_draw_shaded_textured_triangle(400,-200,0,0,0x808080, + 900,100,0,0,0x808080,400,500,0,0,0x808080,0,0,0x108,1); + gl_renderer_note_wide_triangle_recovery(1); + glb_draw_shaded_textured_triangle(561,184,0,0,0x808080, + 1023,-724,0,0,0x808080,1023,334,0,0,0x808080,0,0,0x108,1); + glb_draw_shaded_textured_triangle(40,100,0,0,0x808080, + 70,100,0,0,0x808080,40,130,0,0,0x808080,0,0,0x108,1); + check(glb_vram_read(45,105)==0x03e0,"ordinary textured after oversize textured"); + verify("recovered geometry keeps CPU/GPU readback coherent"); + uint32_t *pixels=calloc((size_t)848*240*scale*scale,sizeof(uint32_t)); + check(pixels!=NULL,"oversize wide pixels allocation"); + if(pixels) { + check(glb_render_wide_display(pixels,848*scale*4,0,0,240)>0,"oversize wide readback"); + check(pixels[(100*scale)*(848*scale)+68*scale]==0xff00ff00u,"left oversize margin drawn in painter order"); + check(pixels[(100*scale)*(848*scale)+818*scale]==0xff00ff00u,"captured right hallway wall drawn"); + check(pixels[(100*scale)*(848*scale)+268*scale]==0xff00ff00u,"canonical center retains recovered faces"); + check(pixels[(100*scale)*(848*scale)+678*scale]==0xff00ff00u,"recovered wall before center-copy boundary"); + check(pixels[(100*scale)*(848*scale)+680*scale]==0xff00ff00u,"same recovered wall after center-copy boundary"); + free(pixels); + } + glb_wide_disable_target(); + gl_renderer_note_wide_triangle_recovery(1); + glb_draw_flat_triangle(-300,-200,300,100,-100,500,0x001f); + check(glb_vram_read(100,100)==0x001f,"admitted backend geometry remains coherent without mirror"); + glb_wide_set_target(0); +} +static void verify_camera_plane_clip(int scale) { + glb_set_draw_area(0,0,511,239);glb_wide_configure(848,168); + glb_wide_set_target(0);glb_wide_set_view(0,0,0,0); + glb_wide_clear(0,0,240,0);glb_draw_flat_rect(-168,0,848,240,0x7c00); + glb_vram_write(512,0,0x03e0); + /* One corner is behind the camera, with two at z=2 in front. The + * visible trapezoid contains (100,100), and extends into the left margin. */ + const PSXProjectedVertex crossing[3]={ + {-1600,0,-1,0,0,1,1,1},{600,0,2,0,0,1,1,1},{600,480,2,0,0,1,1,1}}; + check(gl_renderer_projective_supported(),"projective GPU path ready"); + gl_renderer_draw_projected_triangle(crossing,0x108,0,0,1,-1,1); + check(glb_vram_read(100,100)==0x03e0,"crossing wall retained in front of camera"); + check(glb_vram_read(400,100)==0x7c00,"outside crossing wall remains untouched"); + const PSXProjectedVertex hidden[3]={ + {-400,0,-2,0,0,1,1,1},{600,0,-1,0,0,1,1,1},{600,480,-2,0,0,1,1,1}}; + gl_renderer_draw_projected_triangle(hidden,0x108,0,0,1,-1,1); + check(glb_vram_read(400,100)==0x7c00,"entirely behind-camera triangle rejected"); + glb_draw_flat_rect(120,100,4,4,0x001f); + check(glb_vram_read(121,101)==0x001f,"ordinary draw after clipped wall keeps painter order"); + check(!s_pc_valid && !s_pq_valid && !s_projected_uv_valid,"clipped overrides consumed"); + verify("camera-plane clipping preserves GPU and CPU authority"); + uint32_t *pixels=calloc((size_t)848*240*scale*scale,sizeof(uint32_t)); + check(pixels!=NULL,"camera-plane wide allocation"); + if(pixels) { + check(glb_render_wide_display(pixels,848*scale*4,0,0,240)>0,"camera-plane wide readback"); + check(pixels[100*scale*(848*scale)+68*scale]==0xff00ff00u,"crossing wall retained in wide margin"); + check(pixels[100*scale*(848*scale)+268*scale]==0xff00ff00u,"same crossing wall across center copy"); + free(pixels); + } + /* Perspective UVs must survive intersections without integer rounding. + * Analytic barycentric values at the two samples are (15.789,5.702) + * and (17.5,6.771), independent of the clipped polygon's triangulation. */ + for(int v=0;v<32;++v) for(int u=0;u<32;++u) + glb_vram_write(512+u,v,(uint16_t)((u+1)|((v+1)<<5))); + PSXProjectedVertex mapped[3]={crossing[0],crossing[1],crossing[2]}; + mapped[1].u=mapped[2].u=20;mapped[2].v=20; + gl_renderer_draw_projected_triangle(mapped,0x108,0,0,1,-1,1); + check(glb_vram_read(100,100)==(16|(6<<5)),"fractional clipped UV sample one"); + check(glb_vram_read(200,100)==(18|(7<<5)),"fractional clipped UV sample two"); +} int main(int argc,char **argv){ int scale=argc>1?atoi(argv[1]):1; if(SDL_Init(SDL_INIT_VIDEO)!=0)return 2; @@ -178,6 +270,8 @@ int main(int argc,char **argv){ check(glb_vram_read(482,252)==0x001f,"palette update visible without replacing indices"); verify("retained indices with animated guest CLUT"); verify_bank_batching(); + verify_oversize_wide_geometry(scale); + verify_camera_plane_clip(scale); /* World and UI use different origins in an anchored wide frame. Keep the * canonical-center optimization enabled to catch an erroneous blit over the * completed mirror, and change origins with a pending flat batch. */ diff --git a/runtime/tests/test_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index 242dcfab..58622e6e 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -697,6 +697,21 @@ int test_saturated_nclip_keeps_architectural_result() { if (cpu.gte_data[24] != 0 || !gte_nclip_native_wide_sign(0, &sign) || sign != 1 || gte_nclip_native_wide_sign(1, &sign)) return fail_value("saturated winding rescue preserves native zero MAC0",0,6,0,0,cpu.gte_data[24]); + /* Both quad MAC0 values can saturate to the same zero. Keep each exact + * sign separately: the preceding triangle points forward, latest back. */ + for (unsigned i=0;i<3;i++) { + uint32_t packed=1023u | ((uint32_t)py[2-i]<<16); + cpu.gte_data[12+i]=packed; + gte_test_seed_precise_projection(i,packed,px[2-i]*65536,py[2-i]*65536,1000); + } + gte_execute(&cpu,6); + if (cpu.gte_data[24]!=0 || !gte_nclip_native_wide_sign(0,&sign) || sign!=-1 || + !gte_nclip_native_wide_previous_sign(0,&sign) || sign!=1 || + gte_nclip_native_wide_previous_sign(1,&sign)) + return fail_value("quad retains independent saturated winding results",0,6,0,0,cpu.gte_data[24]); + gte_execute(&cpu,6); + if (!gte_nclip_native_wide_previous_sign(0,&sign) || sign!=-1) + return fail_value("preceding winding advances one command only",0,6,0,0,1); cpu.gte_data[17] = 10; gte_execute(&cpu, 6); if (gte_nclip_native_wide_sign(0, &sign) || cpu.gte_data[24] != 0) @@ -704,7 +719,7 @@ int test_saturated_nclip_keeps_architectural_result() { cpu.gte_data[17] = 1000; gte_execute(&cpu, 6); gte_precision_timeline_invalidate(); - if (gte_nclip_native_wide_sign(0, &sign)) + if (gte_nclip_native_wide_sign(0, &sign) || gte_nclip_native_wide_previous_sign(0,&sign)) return fail_value("timeline invalidation clears winding rescue",0,6,0,0,1); gte_test_seed_precise_projection(0, cpu.gte_data[12] ^ 1u, px[0]*65536, py[0]*65536, 1000); gte_execute(&cpu, 6); @@ -1082,6 +1097,28 @@ int main() { 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; + /* Signed host projection spans the camera plane without changing any + * architectural register, including unsigned SZ and divider/screen flags. */ + pgxp_set_enabled(1); + for (int depth : {-158,-1,0,1,149,150,151,900}) { + GTEState native{}, enhanced{}; + native.RT[0][0]=native.RT[1][1]=native.RT[2][2]=4096; + native.V0[0]=100;native.V0[1]=30;native.TR[2]=depth; + native.H=300;native.OFX=256*65536;native.OFY=120*65536; + enhanced=native; + pgxp_set_projection_tracking(0); + PSXRecomp::GTE::gte_rtps(&native,0x0180001u); + pgxp_set_projection_tracking(1); + PSXRecomp::GTE::gte_rtps(&enhanced,0x0180001u); + PGXPProjection p; + if (std::memcmp(&native,&enhanced,sizeof native) || + !pgxp_get_gte_projection(2,enhanced.SXY[2],&p) || + p.z!=depth || p.x!=256*depth+30000 || p.y!=120*depth+9000 || p.near_z!=150) { + std::fprintf(stderr,"FAIL signed camera projection at depth %d\n",depth); + return 1; + } + } + pgxp_set_projection_tracking(0); std::puts("PASS: canonical GTE register helpers match GTEState transfer oracle"); return 0; } diff --git a/runtime/tests/test_native_wide_projection.c b/runtime/tests/test_native_wide_projection.c index 4d1e04d2..ad2225fb 100644 --- a/runtime/tests/test_native_wide_projection.c +++ b/runtime/tests/test_native_wide_projection.c @@ -82,10 +82,87 @@ int main(void) { prepare_precise_triangle(7,4,10,bx,by); assert(!fixture_precise_triangle.enabled); /* native-wide selected, zero reveal */ configure_native_wide_16_9(); + /* Captured corridor wall: height908 exceeds the PS1 limit, but its + * original projected portion occupies the right widescreen margin. */ + fixture_backend=GR_BACKEND_OPENGL; + hres1=2; + psx_mod_set_native_wide_projection_correction(1); + gp0_cmd_buf[0]=0x3C808080u; + const int32_t wall_x[3]={818,1023,561}, wall_y[3]={-258,-724,184}; + for(unsigned i=0;i<3;i++) { + const unsigned k=1+3*i; + fixture_projections[i].addr=gp0_cmd_source_addr+4*k; + fixture_projections[i].word=gp0_cmd_buf[k]=pack_vertex(wall_x[i],wall_y[i]); + fixture_projections[i].x16=wall_x[i]*65536; + fixture_projections[i].y16=wall_y[i]*65536; + fixture_projections[i].z=900; + fixture_projections[i].valid=1; + } + assert(psx_gpu_triangle_oversize(wall_x,wall_y,0,1,2)); + assert(!gpu_triangle_rejected(wall_x,wall_y,0,1,2)); + prepare_precise_triangle(1,4,7,wall_x,wall_y); + assert(fixture_triangle_recovered); + fixture_backend=GR_BACKEND_SOFTWARE; + assert(gpu_triangle_rejected(wall_x,wall_y,0,1,2)); + fixture_backend=GR_BACKEND_OPENGL; + fixture_projections[1].z=0; + assert(gpu_triangle_rejected(wall_x,wall_y,0,1,2)); + fixture_projections[1].z=900; + fixture_projections[1].word^=1; + assert(gpu_triangle_rejected(wall_x,wall_y,0,1,2)); + fixture_projections[1].word^=1; + fixture_projections[1].y16=-1200*65536; + assert(gpu_triangle_rejected(wall_x,wall_y,0,1,2)); + fixture_projections[1].y16=wall_y[1]*65536; + fixture_projections[1].word=gp0_cmd_buf[4]=pack_vertex(wall_x[1],1023); + fixture_projections[1].y16=1023*65536; + assert(gpu_triangle_rejected(wall_x,wall_y,0,1,2)); + fixture_projections[1].word=gp0_cmd_buf[4]=pack_vertex(wall_x[1],wall_y[1]); + fixture_projections[1].y16=wall_y[1]*65536; + fixture_projections[1].x16=4096*65536-1; + assert(gpu_triangle_rejected(wall_x,wall_y,0,1,2)); + fixture_projections[1].x16=wall_x[1]*65536; + ws_cfg_num=4;ws_cfg_den=3; + assert(gpu_triangle_rejected(wall_x,wall_y,0,1,2)); + configure_native_wide_16_9(); + gp0_cmd_source_addr=UINT32_MAX; + assert(gpu_triangle_rejected(wall_x,wall_y,0,1,2)); psx_mod_set_native_wide_projection_correction(0); assert(!fixture_pgxp_enabled); prepare_precise_triangle(7,4,10,bx,by); + assert(!fixture_triangle_recovered); assert(!fixture_precise_triangle.enabled); + /* A camera-plane crossing needs intact homogeneous data for ALL corners. + * The native-wide clip opt-in is independent of horizontal recovery. */ + psx_mod_set_native_wide_projection_correction(1); + configure_native_wide_16_9(); + fixture_projective_supported=1;fixture_backend=GR_BACKEND_OPENGL; + gp0_cmd_source_addr=0x10004u;gp0_cmd_buf[0]=0x3c808080; + for(unsigned i=0;i<4;++i) { + unsigned k=1+3*i; + gp0_cmd_buf[k]=pack_vertex(100+i*30,40+i*20); + fixture_homogeneous[i].addr=gp0_cmd_source_addr+4*k; + fixture_homogeneous[i].word=gp0_cmd_buf[k]; + fixture_homogeneous[i].p=(PGXPProjection){40000,8000,i==1?-62:500,150}; + fixture_homogeneous[i].valid=1; + } + assert(!native_wide_projective_draw()); + psx_mod_set_native_wide_near_clip(1); + assert(native_wide_projective_draw() && fixture_projective_draws==2); + assert(fixture_projective_vertices[1].z==-62); /* behind-camera depth retained */ + gp0_cmd_buf[4]^=1; + assert(!native_wide_projective_draw()); + gp0_cmd_buf[4]^=1;fixture_homogeneous[2].valid=0; + assert(!native_wide_projective_draw()); + fixture_homogeneous[2].valid=1;fixture_backend=GR_BACKEND_SOFTWARE; + assert(!native_wide_projective_draw()); + fixture_backend=GR_BACKEND_OPENGL;ws_cfg_num=4;ws_cfg_den=3; + assert(!native_wide_projective_draw()); + configure_native_wide_16_9();fixture_projective_supported=0; + assert(!native_wide_projective_draw()); + fixture_projective_supported=1;gp0_cmd_source_addr=UINT32_MAX; + assert(!native_wide_projective_draw()); + assert(fixture_projective_draws==2); puts("native_wide_projection_test: PASS"); return 0; } diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp index 69444f6a..aca3f823 100644 --- a/runtime/tests/test_pgxp.cpp +++ b/runtime/tests/test_pgxp.cpp @@ -566,6 +566,33 @@ int main(void) { CHECK(lookup(ADDR_B, packed, 1023, 152, &x, &y, &z) == PGXP_SRC_NATIVE); } + /* Signed camera depth survives exact word transport and sandbox rollback; + * byte-identical partial writes must still discard the 3D association. */ + { + PGXPProjection p={12345.5f,-7311.0f,-62.0f,150.0f}, out; + pgxp_set_projection_tracking(1); + pgxp_gte_push_sxy(X16,Y16,0,PACKED); + pgxp_gte_set_projection(&p); + CHECK(pgxp_get_gte_projection(2,PACKED,&out) && out.z==-62.0f); + CHECK(!pgxp_get_gte_projection(2,PACKED^1,&out)); + psx_pgxp_cop2(nullptr,MFC2(9,14),PACKED,0); + psx_pgxp_store(nullptr,SW(1,9),ADDR_B,PACKED); + CHECK(pgxp_load_projection(ADDR_B,PACKED,&out) && out.x==p.x && out.z==p.z); + CHECK(!pgxp_load_projection(ADDR_B,PACKED^1,&out)); + pgxp_checkpoint_begin(); + psx_pgxp_store(nullptr,SB(1,9),ADDR_B,PACKED&255); + CHECK(!pgxp_load_projection(ADDR_B,PACKED,&out)); + pgxp_checkpoint_rollback(); + CHECK(pgxp_load_projection(ADDR_B,PACKED,&out) && out.z==p.z); + psx_pgxp_store(nullptr,SH(1,9),ADDR_B,PACKED&65535); + CHECK(!pgxp_load_projection(ADDR_B,PACKED,&out)); + pgxp_store_gte_reg(ADDR_B,14); + CHECK(pgxp_load_projection(ADDR_B,PACKED,&out)); + pgxp_invalidate_all(); + CHECK(!pgxp_load_projection(ADDR_B,PACKED,&out)); + pgxp_set_projection_tracking(0); + } + /* --- stats sanity: dataflow hits were counted --- */ { PGXPStats st; diff --git a/runtime/tests/test_ws_cull_edge_interp.c b/runtime/tests/test_ws_cull_edge_interp.c index 21853506..b1e01206 100644 --- a/runtime/tests/test_ws_cull_edge_interp.c +++ b/runtime/tests/test_ws_cull_edge_interp.c @@ -39,6 +39,10 @@ uint32_t g_debug_current_func_addr; uint32_t g_debug_last_store_pc; uint32_t g_dirty_ram_code_gen; uint64_t g_dispatch_static_hits; +uint32_t g_psx_mod_instruction_hooks; +void psx_mod_instruction(CPUState *cpu, uint32_t pc, uint32_t instruction) { + (void)cpu; (void)pc; (void)instruction; abort(); +} int g_rfe_escape_pending; int g_exc_escape_reason; int g_psx_call_bail; @@ -69,9 +73,13 @@ int dirty_ram_is_dirty(uint32_t phys) { (void)phys; return 1; } int mdec_recently_active(uint32_t f) { (void)f; return 0; } void gte_execute(CPUState *cpu, uint32_t cmd) { (void)cpu; (void)cmd; } static int precise_sign_valid, precise_sign; +static int previous_sign_valid, previous_sign; int gte_nclip_native_wide_sign(int32_t mac0, int* sign) { (void)mac0; *sign = precise_sign; return precise_sign_valid; } +int gte_nclip_native_wide_previous_sign(int32_t mac0, int* sign) { + (void)mac0; *sign = previous_sign; return previous_sign_valid; +} void gte_precision_store_word(uint32_t a, uint8_t r) { (void)a; (void)r; } uint32_t gte_read_ctrl(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; } uint32_t gte_read_data(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; } @@ -439,6 +447,18 @@ static void test_wide_nclip_branches(void) { } precise_sign_valid=0; gpu_ws_set_margin_override(53); cpu.gpr[A0]=0; CHECK(branch_taken(&cpu,sites[0],words[0]), "missing precision stays native"); + precise_sign_valid=1; precise_sign=-1; + previous_sign_valid=1; previous_sign=1; + psx_mod_set_native_wide_nclip_previous_site(sites[1],words[1]); + CHECK(branch_taken(&cpu,sites[1],words[1]), "first quad branch selects preceding positive winding"); + CHECK(!branch_taken(&cpu,sites[2],words[2]), "second quad branch selects latest negative winding"); + previous_sign_valid=0; + CHECK(!branch_taken(&cpu,sites[1],words[1]), "missing preceding winding cannot use latest result"); + previous_sign_valid=1; gpu_ws_set_margin_override(0); + CHECK(!branch_taken(&cpu,sites[1],words[1]), "preceding winding is disabled in native 4:3"); + gpu_ws_set_margin_override(53); + psx_mod_set_native_wide_nclip_sites(sites,words,3); + CHECK(!branch_taken(&cpu,sites[1],words[1]), "registration resets preceding-result bindings"); psx_mod_set_native_wide_nclip_sites(NULL,NULL,0); ws_precise_nclip_cfg=0; ws_xnum=ws_xden=1; gpu_ws_set_margin_override(-1);