diff --git a/docs/NATIVE-WIDE-PROJECTION.md b/docs/NATIVE-WIDE-PROJECTION.md index 17de84c8..2abc7ea3 100644 --- a/docs/NATIVE-WIDE-PROJECTION.md +++ b/docs/NATIVE-WIDE-PROJECTION.md @@ -33,3 +33,27 @@ ordinary conservative geometry consumer continues to reject it. This policy cannot recover a projection whose provenance has already been lost through unsupported CPU arithmetic, or repair a guest culling decision made before GPU submission. Those paths still need title-specific qualification. + +## Saturation-aware winding consumers + +A title can separately bind exact branch words with +`psx_mod_set_native_wide_nclip_sites(addresses, expected, count)` and list its +BLEZ, BGTZ or BGEZ consumers under `[widescreen.cull] nclip_exact_sites`. +Enable `[widescreen] precise_nclip = true` to collect checked GTE projections. +No sites are bound by default. Architectural NCLIP MAC0 and FLAG remain stock; +only the selected rendering branch consumes the corrected winding. + +This narrower predicate requires a matching native MAC0, live projection +generation, matching packed words and integer Y, positive depth outside the +GTE near region, bounded precision coordinates, and an X projection that +exceeds its corresponding saturation rail. It leaves ordinary subpixel winding +unchanged. Zero reveal, stale/missing precision, altered branch words, other +addresses, speculative execution and timeline invalidation retain the native +decision. Both triangle rejection and the quad's winding keep branches need +to be identified from the title's renderer. + +Generated functions, cached overlay callbacks and the interpreter use the same +full-word-bound host predicate. Overlay callback ABI v27 and codegen version 14 +prevent older cached modules from hiding the new forwarding surface. +The `ws_nw` diagnostic also reports `nclip_rescues`, the number of branch +decisions corrected since the title last bound its sites. diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index dcc7999c..4dfdf642 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -821,6 +821,13 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad // sweeps data-as-code into a function; emitting the hardware decode keeps // the regen alive AND matches the oracle if the word is ever executed. check_explicit_branch_site(addr, instr); + if (config_.ws_cull_nclip_exact_sites.count(addr) && + (opcode == 0x06 || opcode == 0x07 || + (opcode == 0x01 && ((instr >> 16) & 31) == 1))) { + const char* compare = opcode == 0x06 ? "<= 0" : opcode == 0x07 ? "> 0" : ">= 0"; + return fmt::format("psx_ws_nclip_branch(0x{:08X}u, 0x{:08X}u, (int32_t){}, ((int32_t){} {})) /* guarded wide nclip */", + addr, instr, reg_name(rs), reg_name(rs), compare); + } if (opcode == 0x01) { uint32_t regimm_op = (instr >> 16) & 0x1F; if ((regimm_op & 0x01u) == 0x00u) { // bltz family (incl. bltzal + undefined mirrors) @@ -3450,6 +3457,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const { ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n"; ss << "extern int psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n"; ss << "extern int psx_ws_masked_reject(uint32_t flags, uint32_t mask); /* guarded packed-coordinate reject */\n"; + ss << "extern int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);\n"; ss << "extern void psx_datashard_ret(CPUState* cpu); /* data-shard capture finalize */\n"; ss << "extern int psx_vsync_query_hle_enter(CPUState* cpu, uint32_t func, uint32_t counter_addr, uint32_t gpustat_ptr_addr, uint32_t timer1_ptr_addr, uint32_t timer1_cache_addr); /* load_accel.c */\n"; ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n"; diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp index 11f4a198..a827ce49 100644 --- a/recompiler/tests/recompiler_patch_test.cpp +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -822,6 +822,12 @@ void codegen_tests() { std::string::npos && nclip_exact.find("ws exact nclip") != std::string::npos, "codegen emits title-scoped exact NCLIP predicate"); + for (uint32_t word : {0x18400002u, 0x1C400002u, 0x04410002u}) { + const std::string guarded = generate_first_instruction(word, {}, false, nclip_exact_config); + check(guarded.find("psx_ws_nclip_branch(0x80010000u,") != std::string::npos && + guarded.find("guarded wide nclip") != std::string::npos, + "inclusive/keep NCLIP branches carry full-word identity to the host"); + } PSXRecomp::CodeGenConfig plane_nx_config; plane_nx_config.ws_cull_plane_nx_sites.insert(0x80010000u); diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 90f458df..8c6665f7 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -239,6 +239,8 @@ extern void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks, * only when it belongs to the supplied native MAC0; otherwise preserves the * 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 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). * The emitter writes them unconditionally; they expand to real calls only diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index a5890d0c..e5f1903e 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -339,6 +339,8 @@ void gpu_ws_set_masked_reject_sites(const uint32_t *addresses, int psx_ws_masked_reject(uint32_t flags, uint32_t mask); int psx_ws_masked_reject_site(uint32_t pc, uint32_t instr, uint32_t flags, int vanilla); +int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla); +uint64_t gpu_ws_native_wide_nclip_rescues(void); int psx_ws_is_cull_clip_edge_x_load_site(uint32_t pc); uint32_t psx_ws_clip_edge_width(void); uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced); diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index d6b7b987..e7f798f1 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -113,6 +113,11 @@ int32_t psx_mod_widescreen_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); +/* 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); /* 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 diff --git a/runtime/include/overlay_api.h b/runtime/include/overlay_api.h index 12bede09..3ff8fb33 100644 --- a/runtime/include/overlay_api.h +++ b/runtime/include/overlay_api.h @@ -116,7 +116,8 @@ * retaining the v24 last_store_pc pointer. */ /* v26: guarded packed-coordinate rejection callback, preserving the selected * rejection axes while the native-wide renderer clips horizontal margins. */ -#define PSX_OVERLAY_ABI_VERSION 26 +/* v27: full-word-bound native-wide winding branch callback. */ +#define PSX_OVERLAY_ABI_VERSION 27 /* Process-lifetime overlay candidate capacity. Every accepted manifest F * record consumes one slot, even when another DLL carries an identical @@ -183,7 +184,8 @@ * so packaged pairs cannot be hidden by an older user cache on first boot. */ /* 13: merge adaptive backdrop v12: independent quotient consumers, proven * copied/biased camera inputs, and two biased bounds for terrain strips. */ -#define PSX_OVERLAY_CODEGEN_VER 13 +/* 14: guarded saturation-aware NCLIP branch consumers and ABI v27 forwarding. */ +#define PSX_OVERLAY_CODEGEN_VER 14 /* Auto codegen hash (generated by hash_codegen.cmake from the recompiler codegen * sources). Folded into the cache PATH next to cg (gcc//cg_/) @@ -360,6 +362,7 @@ typedef struct OverlayCallbacks { * do. NULL leaves the DLL writing a private copy nothing reads. */ uint32_t *last_store_pc; int (*ws_masked_reject)(uint32_t flags, uint32_t mask); + int (*ws_nclip_branch)(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla); } OverlayCallbacks; #ifdef __cplusplus diff --git a/runtime/include/overlay_dispatch_preamble.c.inc b/runtime/include/overlay_dispatch_preamble.c.inc index d1293371..9c854cee 100644 --- a/runtime/include/overlay_dispatch_preamble.c.inc +++ b/runtime/include/overlay_dispatch_preamble.c.inc @@ -268,6 +268,9 @@ uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced) { int psx_ws_masked_reject(uint32_t flags, uint32_t mask) { return g_cbs.ws_masked_reject ? g_cbs.ws_masked_reject(flags, mask) : flags != 0u; } +int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla) { + return g_cbs.ws_nclip_branch ? g_cbs.ws_nclip_branch(pc, instr, mac0, vanilla) : vanilla; +} uint32_t psx_ws_aspect_cone_result(uint32_t site, uint32_t vanilla, uint32_t object, int32_t x, int32_t z, int32_t y) { diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index ebd1a17c..a8396791 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -8081,9 +8081,10 @@ 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}", + "\"projection_correction\":%d,\"projection_vertices\":%llu,\"nclip_rescues\":%llu}", id, psx_ws_get_native_wide(), ws.mode, ws.nw_extra, - correction, (unsigned long long)corrected_vertices); + correction, (unsigned long long)corrected_vertices, + (unsigned long long)gpu_ws_native_wide_nclip_rescues()); } /* Live scanline post-process toggle (A/B): `scanline on=<0|1> pct=<0..100>`. diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 1cc391c5..b92cef45 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -1986,6 +1986,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x06: { /* BLEZ */ int taken = ws_branch_keep(pc, (int32_t)cpu->gpr[rs] <= 0); + taken = psx_ws_nclip_branch(pc, insn, (int32_t)cpu->gpr[rs], taken); exec_delay_slot(cpu, pc + 4); cosim_exec_one_transfer_hook(pc + 4); cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8); @@ -1993,6 +1994,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x07: { /* BGTZ */ int taken = ws_branch_keep(pc, (int32_t)cpu->gpr[rs] > 0); + taken = psx_ws_nclip_branch(pc, insn, (int32_t)cpu->gpr[rs], taken); exec_delay_slot(cpu, pc + 4); cosim_exec_one_transfer_hook(pc + 4); cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8); @@ -2026,6 +2028,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, default: return abort_unsupported(pc, insn, "REGIMM rt"); } taken = ws_branch_keep(pc, taken); + taken = psx_ws_nclip_branch(pc, insn, (int32_t)cpu->gpr[rs], taken); exec_delay_slot(cpu, pc + 4); cosim_exec_one_transfer_hook(pc + 4); cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 0050e433..af14ac07 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -274,7 +274,7 @@ void gpu_ws_set_precise_nclip(int on) { * PGXP correction is disabled. Re-derive the internal transport arm. */ gpu_pgxp_rederive_enable(); } -int gpu_ws_precise_nclip_enabled(void) { return ws_precise_nclip_cfg && ws_active(); } +int gpu_ws_precise_nclip_enabled(void) { return ws_precise_nclip_cfg && (ws_active() || ws_nw_extra() > 0); } void gpu_ws_set_gameplay_state_gate(uint32_t addr, const uint32_t *values, int nvalues) { if (nvalues < 0) nvalues = 0; @@ -883,6 +883,39 @@ int psx_ws_masked_reject_site(uint32_t pc, uint32_t instr, return vanilla; } +static WsMaskedRejectSite ws_nclip_sites[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; } +void psx_mod_set_native_wide_nclip_sites(const uint32_t* addresses, + const uint32_t* expected, int count) { + if (count < 0 || count > WS_EXPLICIT_CULL_SITES_MAX) abort(); + for (int i = 0; i < count; ++i) { + const uint32_t op = expected[i] >> 26, rt = (expected[i] >> 16) & 31; + 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_site_count = count; + ws_nclip_rescues = 0; +} +extern int gte_nclip_native_wide_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 uint32_t op = instr >> 26; + const int result = op == 6 ? sign <= 0 : op == 7 ? sign > 0 : + ((instr >> 16) & 1) ? sign >= 0 : sign < 0; + if (result != vanilla) ++ws_nclip_rescues; + return result; + } + return vanilla; +} + typedef struct { uint32_t address; uint32_t expected; diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index eddf2c32..9826469e 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -940,6 +940,8 @@ static uint64_t s_nclip_disagreements = 0; static int32_t s_nclip_last_native = 0; 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; extern "C" void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks, uint64_t *disagreements) { if (hits) *hits = s_nclip_precise_hits; @@ -951,6 +953,13 @@ extern "C" int gte_nclip_precise_bltz(int32_t native_mac0) { return native_mac0 < 0; return s_nclip_last_precise_sign < 0; } +extern "C" int gte_nclip_native_wide_sign(int32_t native_mac0, int* sign) { + if (!s_nclip_last_precise_valid || !s_nclip_last_horizontal_saturated || + native_mac0 != s_nclip_last_native || s_nclip_last_generation != s_geom_generation || + s_gte_replay_sandbox || s_speculative_depth != 0) return 0; + *sign = s_nclip_last_precise_sign; + return 1; +} void gte_nclip(GTEState* gte, uint32_t instr) { gte->FLAG = 0; @@ -967,6 +976,7 @@ void gte_nclip(GTEState* gte, uint32_t instr) { int32_t out = static_cast(mac0); s_nclip_last_native = out; s_nclip_last_precise_valid = false; + s_nclip_last_horizontal_saturated = false; /* Compute an exact 16.16 determinant for configured branch consumers, but * preserve native guest-visible MAC0 for every architectural reader. */ int32_t px0, py0, px1, py1, px2, py2; @@ -983,6 +993,22 @@ void gte_nclip(GTEState* gte, uint32_t instr) { const int64_t cross = dx10 * dy20 - dy10 * dx20; s_nclip_last_precise_sign = cross < 0 ? -1 : (cross > 0 ? 1 : 0); s_nclip_last_precise_valid = true; + s_nclip_last_generation = s_geom_generation; + /* Narrow rescue: exact Y still agrees with the architectural FIFO, + * depth is outside the GTE near region, and at least one X exceeded + * the matching saturation rail. Ordinary subpixel winding stays stock. */ + const int raw_x[3] = {sx0, sx1, sx2}, raw_y[3] = {sy0, sy1, sy2}; + const int32_t exact_x[3] = {px0, px1, px2}, exact_y[3] = {py0, py1, py2}; + bool checked = true, saturated = false; + for (int i = 0; i < 3; ++i) { + const int x = exact_x[i] >> 16; + checked &= (exact_y[i] >> 16) == raw_y[i] && x > -4096 && x < 4096; + checked &= gte->SZ[i + 1] >= gte->H / 2 && gte->SZ[i + 1] != 0; + if (raw_x[i] == 1023 && x > 1023) saturated = true; + else if (raw_x[i] == -1024 && x < -1024) saturated = true; + else checked &= x == raw_x[i]; + } + s_nclip_last_horizontal_saturated = checked && saturated; if ((cross > 0 && out <= 0) || (cross < 0 && out >= 0)) s_nclip_disagreements++; } else if (gpu_ws_precise_nclip_enabled() && diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index c7fe3888..37510f7b 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -2629,6 +2629,8 @@ static void init_callbacks(void) { { extern int psx_ws_masked_reject(uint32_t flags, uint32_t mask); s_callbacks.ws_masked_reject = psx_ws_masked_reject; + extern int psx_ws_nclip_branch(uint32_t, uint32_t, int32_t, int); + s_callbacks.ws_nclip_branch = psx_ws_nclip_branch; } { extern uint32_t psx_ws_aspect_cone_result( diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index 1cfd7c00..d77e253f 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -335,3 +335,6 @@ int gr_render_wide_display(uint32_t *out, int pitch, int base_x, (void)out; (void)pitch; (void)base_x; (void)disp_y; (void)disp_h; return 0; } +int gte_nclip_native_wide_sign(int32_t mac0, int* sign) { + (void)mac0; (void)sign; return 0; +} diff --git a/runtime/tests/test_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index 0d6415d7..baaded8e 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -625,6 +625,39 @@ int test_precise_nclip_is_title_scoped() { return 0; } +int test_saturated_nclip_keeps_architectural_result() { + CPUState cpu{}; + gte_precision_tracking_set(1); + g_test_precise_nclip_enabled = 1; + cpu.gte_ctrl[26] = 300; + const int px[] = {1100, 1200, 1300}, py[] = {100, 200, 400}; + for (unsigned i=0;i<3;i++) { + uint32_t packed = 1023u | ((uint32_t)py[i] << 16); + cpu.gte_data[12+i] = packed; cpu.gte_data[17+i] = 1000; + gte_test_seed_precise_projection(i, packed, px[i]*65536, py[i]*65536, 1000); + } + gte_execute(&cpu, 6); + int sign = 0; + 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]); + cpu.gte_data[17] = 10; + gte_execute(&cpu, 6); + if (gte_nclip_native_wide_sign(0, &sign) || cpu.gte_data[24] != 0) + return fail_value("near depth retains native reject",0,6,0,0,cpu.gte_data[24]); + cpu.gte_data[17] = 1000; + gte_execute(&cpu, 6); + gte_precision_timeline_invalidate(); + if (gte_nclip_native_wide_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); + if (gte_nclip_native_wide_sign(0, &sign)) + return fail_value("stale NCLIP projection refuses rescue",0,6,0,0,1); + g_test_precise_nclip_enabled = 0; + return 0; +} + int test_precision_speculative_transaction() { constexpr uint32_t address = 0x00123450u; constexpr uint32_t packed = 0x00420021u; @@ -739,6 +772,7 @@ int main() { if (int rc = test_command_timing_hook()) return rc; if (int rc = test_precise_sxy_invalidation()) return rc; if (int rc = test_precise_nclip_is_title_scoped()) return rc; + if (int rc = test_saturated_nclip_keeps_architectural_result()) return rc; if (int rc = test_precision_speculative_transaction()) return rc; std::puts("PASS: canonical GTE register helpers match GTEState transfer oracle"); return 0; diff --git a/runtime/tests/test_ws_cull_edge_interp.c b/runtime/tests/test_ws_cull_edge_interp.c index a2ad1d22..21853506 100644 --- a/runtime/tests/test_ws_cull_edge_interp.c +++ b/runtime/tests/test_ws_cull_edge_interp.c @@ -68,6 +68,10 @@ uint32_t psx_cyc_lwc2_read(CPUState *cpu, uint32_t a) { (void)cpu; return psx_re 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; +int gte_nclip_native_wide_sign(int32_t mac0, int* sign) { + (void)mac0; *sign = precise_sign; return precise_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; } @@ -413,12 +417,40 @@ static void test_masked_reject(void) { gpu_ws_set_margin_override(-1); } +static void test_wide_nclip_branches(void) { + CPUState cpu = {0}; + ws_xnum = 3; ws_xden = 4; ws_mode = 0; + ws_precise_nclip_cfg = 1; + precise_sign_valid = 1; precise_sign = 1; + const uint32_t sites[] = {CODE+0x300,CODE+0x340,CODE+0x380}; + const uint32_t words[] = {i_type(6,A0,0,3),i_type(7,A0,0,3),i_type(1,A0,1,3)}; + psx_mod_set_native_wide_nclip_sites(sites,words,3); + for (int margin=0;margin<=53;margin+=53) { + gpu_ws_set_margin_override(margin); + cpu.gpr[A0]=0; + CHECK(branch_taken(&cpu,sites[0],words[0]) == (margin==0), "BLEZ native zero vs precise positive"); + CHECK(branch_taken(&cpu,sites[1],words[1]) == (margin!=0), "BGTZ native zero vs precise positive"); + CHECK(cpu.gpr[A0]==0, "winding helper leaves guest MAC0 register alone"); + cpu.gpr[A0]=(uint32_t)-1; + CHECK(branch_taken(&cpu,sites[2],words[2]) == (margin!=0), "BGEZ exact keep predicate"); + CHECK(branch_taken(&cpu,sites[0]+4,words[0]), "unlisted twin stays native"); + cpu.gpr[V0]=(uint32_t)-1; + CHECK(branch_taken(&cpu,sites[0],i_type(6,V0,0,3)), "wrong full word stays native"); + } + 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"); + 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); +} + int main(int argc, char **argv) { if (argc > 1) s_capture_path = argv[1]; test_store(); test_helpers(); test_interpreter(); test_masked_reject(); + test_wide_nclip_branches(); if (failures) { fprintf(stderr, "%d check(s) failed\n", failures); return 1; diff --git a/tools/tests/test_overlay_widescreen_callbacks.py b/tools/tests/test_overlay_widescreen_callbacks.py index 3867e4f9..9bf46665 100644 --- a/tools/tests/test_overlay_widescreen_callbacks.py +++ b/tools/tests/test_overlay_widescreen_callbacks.py @@ -22,6 +22,10 @@ static uint32_t seen_address, cycles; static int32_t bound(int32_t x) { return x * 2; } static int margin120(void) { return 120; } static int packed_reject(uint32_t flags, uint32_t mask) { return (flags & mask) != 0; } +static int nclip_branch(uint32_t pc, uint32_t word, int32_t mac0, int vanilla) { + assert(pc == 0x80021EAC && word == 0x1900FFCF && mac0 == 0 && vanilla == 1); + return 0; +} static void advance(uint32_t n) { cycles += n; } static int entry(CPUState *cpu, uint32_t address) { assert(cycles == 17); seen_cpu = cpu; seen_address = address; @@ -66,6 +70,10 @@ int main(void) { assert(psx_ws_masked_reject(0xFE00u, 0xFFFF0000u) == 0); assert(psx_ws_masked_reject(0xFF00FE00u, 0xFFFF0000u) == 1); assert(psx_ws_masked_reject(0u, 0xFFFF0000u) == 0); + assert(psx_ws_nclip_branch(0x80021EAC, 0x1900FFCF, 0, 1) == 1); + callbacks.ws_nclip_branch = nclip_branch; + overlay_init(&callbacks); + assert(psx_ws_nclip_branch(0x80021EAC, 0x1900FFCF, 0, 1) == 0); return 0; } '''