Clip proven native-wide world faces across camera and screen planes

This commit is contained in:
Matthew Stanley
2026-10-03 23:42:23 -07:00
parent 3b1f9319f3
commit cd9981cda8
17 changed files with 649 additions and 23 deletions
+26
View File
@@ -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.
+1
View File
@@ -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).
+11
View File
@@ -8,6 +8,7 @@
* same struct tag) so this header needs no SDL include. */
#include <stdint.h>
#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]);
+52
View File
@@ -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<n;++i) {
PSXProjectedVertex cur=a[i];
double dc=psx_projective_distance(cur,plane,left,right,top,bottom);
if ((dp>=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<n;++i) a[i]=b[i];
}
for (int i=0;i<n;++i) out[i]=a[i];
return n;
}
#endif
+10
View File
@@ -153,11 +153,21 @@ int32_t psx_mod_widescreen_view_x_margin(void);
* 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);
/* Separately qualify near-camera world clipping for a title. The GL path
* carries signed, unclamped projection through exact PGXP word transport,
* clips at the camera/view planes, and preserves ordered GPU/VRAM writes.
* Requires projection correction above; 4:3, software and untracked UI stay
* on their existing paths. No architectural GTE or gameplay changes. */
void psx_mod_set_native_wide_near_clip(int enabled);
/* Bind render-only NCLIP branch consumers to full instruction words. These
* recover winding only when valid horizontal projections saturated; guest
* MAC0 and flags remain architectural. Empty registration disables the sites. */
void psx_mod_set_native_wide_nclip_sites(const uint32_t* addresses,
const uint32_t* expected, int count);
/* After registering sites, bind a verified first-of-two quad branch to the
* preceding NCLIP result. Exact address/word must match an existing site;
* registering the base site list again resets every site to the latest result. */
void psx_mod_set_native_wide_nclip_previous_site(uint32_t address, uint32_t expected);
/* 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
+10
View File
@@ -162,6 +162,16 @@ void pgxp_suppress_end(void);
* sees. Shifts the shadow FIFO exactly like push_sxy (regs 12..15). */
void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, uint32_t packed);
/* Host-only homogeneous screen projection, before SZ/SXY/divider saturation.
* x/z and y/z are screen pixels; signed z retains camera-plane crossings.
* Exact full-word copies carry it. Arithmetic/partial writes invalidate it. */
typedef struct PGXPProjection { float x, y, z, near_z; } PGXPProjection;
void pgxp_set_projection_tracking(int enabled);
int pgxp_projection_tracking(void);
void pgxp_gte_set_projection(const PGXPProjection *projection);
int pgxp_load_projection(uint32_t addr, uint32_t packed, PGXPProjection *out);
int pgxp_get_gte_projection(uint32_t index, uint32_t packed, PGXPProjection *out);
/* Current SXY FIFO shadow (index 0..3 selects GTE data regs 12..15).
* Returns nonzero when the shadow is live and carries X/Y precision. */
int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16);
+4 -2
View File
@@ -8177,10 +8177,12 @@ static void handle_ws_nw(int id, const char *json)
GpuWsDebug ws;
gpu_ws_get_debug(&ws);
send_fmt("{\"id\":%d,\"ok\":true,\"native_wide\":%d,\"mode\":%d,\"nw_extra\":%d,"
"\"projection_correction\":%d,\"projection_vertices\":%llu,\"nclip_rescues\":%llu}",
"\"projection_correction\":%d,\"projection_vertices\":%llu,\"nclip_rescues\":%llu,"
"\"oversize_triangles\":%llu}",
id, psx_ws_get_native_wide(), ws.mode, ws.nw_extra,
correction, (unsigned long long)corrected_vertices,
(unsigned long long)gpu_ws_native_wide_nclip_rescues());
(unsigned long long)gpu_ws_native_wide_nclip_rescues(),
(unsigned long long)gl_renderer_wide_triangle_recovery_count());
}
/* Live scanline post-process toggle (A/B): `scanline on=<0|1> pct=<0..100>`.
+121 -13
View File
@@ -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<n;++i) {
const unsigned index=1+stride*i;
const uint32_t word=gp0_cmd_buf[index];
PGXPProjection p;
if (!pgxp_load_projection(gp0_cmd_source_addr+4*index,word,&p)) return 0;
int32_t x,y; parse_vertex(word,&x,&y);
needs_clip |= p.z<p.near_z || x==-1024 || x==1023 || y==-1024 || y==1023;
const uint32_t color=gp0_cmd_buf[(op&16u)?index-1:0];
const uint32_t uv=gp0_cmd_buf[index+1];
vertices[i]=(PSXProjectedVertex){p.x+(double)draw_offset_x*p.z,
p.y+(double)draw_offset_y*p.z,p.z,uv&255,(uv>>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);
+53 -3
View File
@@ -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].u<limits[0]) limits[0]=(int)vertices[i].u;
if (vertices[i].v<limits[1]) limits[1]=(int)vertices[i].v;
if (vertices[i].u>limits[2]) limits[2]=(int)vertices[i].u;
if (vertices[i].v>limits[3]) limits[3]=(int)vertices[i].v;
}
for (int k=1;k+1<n;++k) {
const PSXProjectedVertex v[3]={polygon[0],polygon[k],polygon[k+1]};
int xs[3],ys[3],us[3],vs[3]; float col[9];
s_pc_valid=1; s_pq_valid=perspective; s_projected_uv_valid=1;
for (int i=0;i<3;++i) {
s_pc_x[i]=(float)(v[i].x/v[i].z);
s_pc_y[i]=(float)(v[i].y/v[i].z);
xs[i]=(int)floorf(s_pc_x[i]); ys[i]=(int)floorf(s_pc_y[i]);
s_pq[i]=(float)(1.0/v[i].z);
s_projected_u[i]=(float)v[i].u; s_projected_v[i]=(float)v[i].v;
us[i]=(int)v[i].u; vs[i]=(int)v[i].v;
col[i*3]=(float)v[i].r; col[i*3+1]=(float)v[i].g; col[i*3+2]=(float)v[i].b;
}
gpu_textured_triangle(xs,ys,us,vs,col,texpage,cx,cy,raw,semi,limits);
precise_consumed();
}
precise_consumed();
}
static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) {
if (cpu_raster_required())
+41
View File
@@ -240,7 +240,9 @@ static uint32_t s_geom_miss_ambig = 0; /* recorded, but not unambiguously *
static uint32_t s_speculative_depth = 0;
static int s_speculative_timeline_invalidated = 0;
static void gte_nclip_precision_invalidate();
static void gte_geom_generation_advance(void) {
gte_nclip_precision_invalidate();
if (++s_geom_generation == 0) {
if (s_geom_cache)
std::memset(s_geom_cache, 0, GEOM_CACHE_SIZE * sizeof(GeomVertex));
@@ -1006,6 +1008,19 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
}
}
pgxp_gte_push_sxy(px16, py16, gte->SZ[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;
+34 -3
View File
@@ -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;
+30 -1
View File
@@ -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;
}
+94
View File
@@ -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. */
+38 -1
View File
@@ -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;
}
@@ -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;
}
+27
View File
@@ -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;
+20
View File
@@ -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);