diff --git a/docs/config_schema.md b/docs/config_schema.md index 5f987226..f0a2fe07 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -220,7 +220,8 @@ them. ### Guarded packed-coordinate rejection -Some polygon funnels combine packed SXY flags and reject on `BNE flags,zero`. +Some polygon funnels combine packed SXY flags and reject on `BNE flags,zero`, +or read the GTE error summary with `BLTZ flags`. The native-wide GPU can clip horizontal overflow while the game retains its vertical rejection bits. Declare only verified render rejection branches: @@ -237,6 +238,10 @@ wide gameplay only the specified flag bits participate in the predicate; guest registers, the branch delay slot, vertical/depth/backface tests and timing remain intact. Adapters must provide sufficient bounded primitive/capture capacity. Changing a site, guard or mask changes the overlay configuration fingerprint. +For a verified GTE `BLTZ` consumer, mask the underlying error bits rather than +bit 31 itself. For example, `0x7F87A000` preserves the hardware error constituents +except horizontal SX saturation. Positive non-summary flags retain their original +acceptance at 4:3. Link branches and other REGIMM forms are not supported. ### Guarded widescreen participation comparisons diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 599c240a..1815a9a5 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -803,6 +803,11 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad fmt::print(stderr, "ERROR: masked_reject instruction mismatch at 0x{:08X}\n", addr); std::exit(1); } + // BLTZ GTE error-summary consumers retain the signed original + // predicate at 4:3. Non-summary positive flags are not a reject. + if (opcode == 1u) + return fmt::format("psx_ws_x_margin() > 0 ? psx_ws_masked_reject({}, 0x{:08X}u) : ((int32_t){} < 0)", + reg_name(rs), site.reject_mask, reg_name(rs)); return fmt::format("psx_ws_masked_reject({}, 0x{:08X}u)", reg_name(rs), site.reject_mask); } diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 37b068df..83f1a9d2 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -2051,10 +2051,12 @@ GameConfig load_game_config(const fs::path& config_path_in) { "widescreen.cull.masked_reject.expected"); site.reject_mask = parse_hex(toml::find(item, "reject_mask"), "widescreen.cull.masked_reject.reject_mask"); - if ((site.address & 3u) || (site.expected >> 26) != 5u || + const uint32_t opcode = site.expected >> 26; + const bool supported = opcode == 5u || opcode == 1u; + if ((site.address & 3u) || !supported || ((site.expected >> 16) & 31u) != 0u || ((site.expected >> 21) & 31u) == 0u || !site.reject_mask) - throw std::runtime_error("masked_reject needs aligned BNE reg,zero and nonzero reject_mask"); + throw std::runtime_error("masked_reject needs aligned BNE reg,zero or BLTZ reg and nonzero reject_mask"); if (!seen.insert(site.address & 0x1FFFFFFFu).second) throw std::runtime_error("duplicate masked_reject address"); ws_cull_masked_reject_sites.push_back(site); diff --git a/recompiler/tests/ws_cull_edge_codegen_test.cpp b/recompiler/tests/ws_cull_edge_codegen_test.cpp index c0655eec..295bf4f1 100644 --- a/recompiler/tests/ws_cull_edge_codegen_test.cpp +++ b/recompiler/tests/ws_cull_edge_codegen_test.cpp @@ -413,6 +413,29 @@ reject_mask = "0xFFFF0000" const auto other = generate_first_instruction(0x14800003u, config).full_code; check(other.find("psx_ws_masked_reject(") == std::string::npos, "overlay with another instruction stays vanilla"); + + const std::string flag_body = plain + R"toml( +[[widescreen.cull.masked_reject]] +address = "0x80010000" +expected = "0x04400002" +reject_mask = "0x7F87A000" +)toml"; + const auto flag_config = load(flag_body); + config.ws_cull_masked_reject_sites = flag_config.ws_cull_masked_reject_sites; + config.overlay_mode = false; + const auto flags = generate_first_instruction(kBltzV0, config).full_code; + check(flags.find("psx_ws_x_margin() > 0 ? psx_ws_masked_reject(cpu->gpr[2], 0x7F87A000u) : ((int32_t)cpu->gpr[2] < 0)") != std::string::npos, + "GTE summary branch preserves signed predicate at 4:3"); + check(load_throws_with(plain + R"toml( +[[widescreen.cull.masked_reject]] +address = "0x80010000" +expected = "0x04500002" +reject_mask = "0x7F87A000" +)toml", "BLTZ reg"), "masked reject excludes link branch"); + config.overlay_mode = true; + const auto mismatched_flags = generate_first_instruction(kBgezV0, config).full_code; + check(mismatched_flags.find("psx_ws_masked_reject(") == std::string::npos, + "GTE overlay opcode mismatch keeps signed vanilla branch"); } void runtime_math() { diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 68e2ec52..07ada995 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -2104,6 +2104,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_masked_reject_site(pc, insn, cpu->gpr[rs], 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); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 3b9c6cf9..47c43286 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -952,6 +952,7 @@ 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) { + if (psx_ws_x_margin() <= 0) return vanilla; for (int i = 0; i < ws_masked_reject_n; ++i) { const WsMaskedRejectSite *s = &ws_masked_reject_sites[i]; if (s->address == (pc & 0x1FFFFFFFu) && s->expected == instr) @@ -2506,13 +2507,18 @@ int gpu_ws_background_stretch_active(void) { frame - ws_background_tag_frame <= WS_HUD_ANCHOR_FRESH_FRAMES; } +/* Legacy title opt-ins also change background sampling inside the original + * viewport. Keep their wide composite intact, just like explicit packet tags. */ +static int ws_nw_phase_backdrop = 0; +static int ws_nw_flat_backdrop = 0; int gpu_ws_background_requires_full_composite(void) { /* Freshness governs reuse of a packet address, not the lifetime of the * pixels it produced. A slow game frame or a held display buffer can keep * those pixels on screen indefinitely. Conservatively keep full mirroring * once a composite has been tagged; GPU reset/snapshot restore clears the * latch together with the tags. Never modify the packet freshness guard. */ - return ws_background_tags_used && ws_native_wide_configured(); + return ws_native_wide_configured() && + (ws_background_tags_used || ws_nw_phase_backdrop || ws_nw_flat_backdrop); } static int ws_nw_explicit_background(void) { @@ -2676,7 +2682,6 @@ static int ws_tagged_anchor(int32_t *out_ax) { * flower-field tiles — the 3D rock/foreground is untagged AND has narrow prims, * so tag/narrow heuristics alone mis-stretch and tear it. */ uint32_t g_ws_backdrop_lo = 0, g_ws_backdrop_hi = 0; -static int ws_nw_phase_backdrop = 0; void gpu_ws_set_nw_phase_backdrop(int on) { ws_nw_phase_backdrop = on ? 1 : 0; } static int ws_nw_textured_edges = 0; int g_ws_tex_edge_pct = 0; @@ -3218,7 +3223,6 @@ static void ws_nw_hud_shift_vertices(int32_t *vx, int count) { * Transforms vx[0..3] IN PLACE (pre-draw_offset); returns 1 if it applied. */ static int ws_nw_backdrop = 0; void gpu_ws_set_nw_backdrop(int on) { ws_nw_backdrop = on ? 1 : 0; } -static int ws_nw_flat_backdrop = 0; void gpu_ws_set_nw_flat_backdrop(int on) { ws_nw_flat_backdrop = on ? 1 : 0; } int gpu_ws_nw_flat_backdrop_enabled(void) { return ws_nw_flat_backdrop; } static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) { diff --git a/runtime/tests/test_ws_cull_edge_interp.c b/runtime/tests/test_ws_cull_edge_interp.c index b1e01206..34d843f3 100644 --- a/runtime/tests/test_ws_cull_edge_interp.c +++ b/runtime/tests/test_ws_cull_edge_interp.c @@ -425,6 +425,31 @@ static void test_masked_reject(void) { gpu_ws_set_margin_override(-1); } +static void test_gte_summary_reject(void) { + CPUState cpu = {0}; + const uint32_t site = CODE + 0x240; + const uint32_t word = i_type(1, A0, 0, 3); + const uint32_t mask = 0x7F87A000u; + gpu_ws_set_masked_reject_sites(&site, &word, &mask, 1); + for (int margin = 0; margin <= 53; margin += 53) { + gpu_ws_set_margin_override(margin); + cpu.gpr[A0] = 0x80004000u; + CHECK(branch_taken(&cpu, site, word) == (margin == 0), "GTE X-only summary at margin %d", margin); + CHECK(cpu.gpr[A0] == 0x80004000u, "GTE mask preserves architectural FLAG copy"); + CHECK(branch_taken(&cpu, site + 0x40, word), "unlisted GTE branch stays vanilla"); + CHECK(!branch_taken(&cpu, site, i_type(1, A0, 1, 3)), "wrong GTE full word keeps BGEZ predicate"); + const uint32_t flags[] = {0x80002000u, 0x80020000u, 0x80040000u, 0xC0000000u}; + for (unsigned i = 0; i < sizeof flags / sizeof flags[0]; ++i) { + cpu.gpr[A0] = flags[i]; + CHECK(branch_taken(&cpu, site, word), "GTE Y/depth flags retain rejection"); + } + cpu.gpr[A0] = 0x00401000u; + CHECK(!branch_taken(&cpu, site, word), "positive non-summary flags do not reject"); + } + gpu_ws_set_masked_reject_sites(NULL, NULL, NULL, 0); + 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; @@ -470,6 +495,7 @@ int main(int argc, char **argv) { test_helpers(); test_interpreter(); test_masked_reject(); + test_gte_summary_reject(); test_wide_nclip_branches(); if (failures) { fprintf(stderr, "%d check(s) failed\n", failures);