From 6f37a0edee407e07de72c36efc0ba7b69e1ce57e Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 12:59:36 -0700 Subject: [PATCH 1/6] Widen guarded packed screen-X predicates in native and interpreted code (cherry picked from commit b0f7b2be8e12d40824a28932d990cfd8e79aa370) --- docs/config_schema.md | 17 +++++++++ recompiler/include/code_generator.h | 1 + recompiler/src/code_generator.cpp | 21 +++++++++++ recompiler/src/config_loader.cpp | 35 +++++++++++++++++++ recompiler/src/config_loader.h | 7 ++++ recompiler/src/main_psx.cpp | 4 +++ .../ws_cull_screen_x_hi_codegen_test.cpp | 22 ++++++++++++ runtime/include/gpu.h | 4 +++ runtime/include/ws_cull_edge.h | 14 ++++++++ runtime/src/dirty_ram_interp.c | 6 ++-- runtime/src/gpu.c | 23 ++++++++++++ runtime/src/main.cpp | 9 +++++ 12 files changed, 161 insertions(+), 2 deletions(-) diff --git a/docs/config_schema.md b/docs/config_schema.md index 1abc6908..d562ebbb 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -241,6 +241,23 @@ Changing a site, guard or mask changes the overlay configuration fingerprint. ### Guarded widescreen participation comparisons +`[[widescreen.cull.packed_x]]` handles a proven mesh screen-X predicate +expressed as `SLTU rd, (SX << 16), (width << 16)`: + +```toml +[[widescreen.cull.packed_x]] +address = "0x801466A0" +expected = "0x0079C02B" +``` + +The complete instruction guards native overlays and the interpreter. While +wide, X must lie in `[-margin, width + margin)`; both sides widen together. +At 4:3 the original unsigned comparison is exact. Nonzero low-half operands, +zero width, and widths above 1024 retain the original comparison. It changes +only the predicate, preserving GPR operands, vertical, backface, depth and +ordering-table limits. Sites affect the overlay cache identity; regenerate +main/native overlays after changing them. Empty lists are inert. + Games may disable a proven object/model cull verdict in widened world views without changing true 4:3 behavior: diff --git a/recompiler/include/code_generator.h b/recompiler/include/code_generator.h index 0896727a..36fa8642 100644 --- a/recompiler/include/code_generator.h +++ b/recompiler/include/code_generator.h @@ -180,6 +180,7 @@ struct CodeGenConfig { // [[widescreen.cull.sxy]] packed-SXY AND folds (left edge widened). std::vector ws_cull_sxy_sites; std::vector ws_cull_masked_reject_sites; + std::vector ws_cull_packed_x_sites; std::vector ws_cull_scale_sites; // Exact `addi[u] rt,zero,imm` 12-bit angular half-extents. The runtime diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 6fa5a161..ae3c1c51 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -1235,6 +1235,25 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in reg_name(dst), vanilla, site.result, comment); } + for (const auto& site : config_.ws_cull_packed_x_sites) { + if ((site.address & 0x1FFFFFFFu) != (addr & 0x1FFFFFFFu)) continue; + if (instr != site.expected) { + if (config_.overlay_mode) continue; + fmt::print(stderr, "ERROR: packed_x expected 0x{:08X} at 0x{:08X}, found 0x{:08X}\n", + site.expected, addr, instr); + std::exit(1); + } + if (opcode != 0u || funct != 0x2Bu) { + fmt::print(stderr, "ERROR: packed_x site 0x{:08X} must be SLTU\n", addr); + std::exit(1); + } + return fmt::format( + "{} = psx_ws_cull_packed_x_value({}, {}, psx_ws_x_margin());" + " /* guarded packed screen-X range */{}", + reg_name(get_rd(instr)), reg_name(get_rs(instr)), + reg_name(get_rt(instr)), comment); + } + // Register-register frustum-edge compares ([[widescreen.cull.scale]]): // the configured operand is a +/-half_extent*z bound computed at runtime, // so it is scaled by the live reveal instead of rewriting an immediate. @@ -3768,6 +3787,8 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const { ss << "extern uint32_t psx_ws_xclip_bound(uint32_t vanilla); /* ws per-prim X reject bound load (gpu.c) */\n"; ss << "extern uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced); /* ws guarded keep compare (gpu.c) */\n"; ss << "extern int32_t psx_ws_cull_scale(int32_t bound, int32_t half_extent); /* ws frustum-edge bound scale (gpu.c) */\n"; + if (!config_.ws_cull_packed_x_sites.empty()) + ss << "#include \"ws_cull_edge.h\"\n"; ss << "extern 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); /* aspect-aware participation cone */\n"; ss << "extern uint32_t psx_ws_angle_widen(uint32_t vanilla); /* aspect-scaled 12-bit terrain-frustum half-angle */\n"; ss << "extern int psx_ws_backdrop_x(int x); /* widescreen backdrop screenX squash (gpu.c) */\n"; diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index dd69e470..63f504cd 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -64,6 +64,18 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) { h.words("sprite_tag_funcs", c.ws_sprite_tag_funcs); h.words("mod_function_entry_funcs", c.mod_function_entry_funcs); + if (!c.ws_cull_packed_x_sites.empty()) { + h.tag("cull_packed_x"); + auto sites = c.ws_cull_packed_x_sites; + std::sort(sites.begin(), sites.end(), [](const auto& a, const auto& b) { + return (a.address & 0x1FFFFFFFu) < (b.address & 0x1FFFFFFFu); + }); + h.u32((uint32_t)sites.size()); + for (const auto& site : sites) { + h.u32(site.address & 0x1FFFFFFFu); + h.u32(site.expected); + } + } if (!c.ws_cull_masked_reject_sites.empty()) { h.tag("cull_masked_reject"); auto sites = c.ws_cull_masked_reject_sites; @@ -2489,6 +2501,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { std::vector ws_cull_keep_sites; std::vector ws_cull_sxy_sites; std::vector ws_cull_masked_reject_sites; + std::vector ws_cull_packed_x_sites; std::vector ws_cull_scale_sites; std::vector ws_cull_angle_sites; WidescreenAspectConeConfig ws_aspect_cone; @@ -2557,6 +2570,27 @@ GameConfig load_game_config(const fs::path& config_path_in) { throw std::runtime_error("masked_reject supports at most 256 sites"); } } + if (cull.contains("packed_x")) { + std::set seen; + for (const auto& item : toml::find(cull, "packed_x")) { + WidescreenPackedXSite site; + site.address = parse_hex(toml::find(item, "address"), + "widescreen.cull.packed_x.address"); + site.expected = parse_hex(toml::find(item, "expected"), + "widescreen.cull.packed_x.expected"); + if ((site.address & 3u) || (site.expected >> 26) != 0u || + (site.expected & 0x7FFu) != 0x2Bu || + ((site.expected >> 21) & 31u) == 0u || + ((site.expected >> 16) & 31u) == 0u || + ((site.expected >> 11) & 31u) == 0u) + throw std::runtime_error("packed_x needs aligned SLTU rd,rs,rt with nonzero registers"); + if (!seen.insert(site.address & 0x1FFFFFFFu).second) + throw std::runtime_error("duplicate packed_x address"); + ws_cull_packed_x_sites.push_back(site); + if (ws_cull_packed_x_sites.size() > 256) + throw std::runtime_error("packed_x supports at most 256 sites"); + } + } if (cull.contains("keep")) { std::set seen; for (const auto& item : toml::find(cull, "keep")) { @@ -3097,6 +3131,7 @@ GameConfig load_game_config(const fs::path& config_path_in) { std::move(ws_cull_clip_edge_x_load_sites); loaded.ws_cull_clip_edge_width = ws_cull_clip_edge_width; loaded.ws_cull_masked_reject_sites = std::move(ws_cull_masked_reject_sites); + loaded.ws_cull_packed_x_sites = std::move(ws_cull_packed_x_sites); loaded.draw_distance_clamp_sites = parse_draw_distance_clamps(cfg, config_path); loaded.quality_presets = parse_quality_presets(cfg, root, config_path); diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 584a08a4..f40de18e 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -1029,6 +1029,11 @@ struct QualityPreset { std::vector keys; // the [video] keys this preset sets }; +struct WidescreenPackedXSite { + uint32_t address = 0; + uint32_t expected = 0; +}; + struct GameConfig { std::filesystem::path config_path; std::filesystem::path project_root; @@ -1547,6 +1552,8 @@ struct GameConfig { std::vector quality_presets; // [[mod_immediate.sites]]: run-time-selectable immediates (ModImmediateSite). std::vector mod_immediate_sites; + // Guarded SLTU on SX<<16 against screen width<<16; both wide edges. + std::vector ws_cull_packed_x_sites; }; // Effective clip_edge_width: explicit value, else screen_w_imms[0], else 320. diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index e5a4d36c..ab22b119 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -246,6 +246,7 @@ static int psxrecomp_game_main(int argc, char** argv) { uint32_t ws_cull_clip_edge_width = 0; // [widescreen.cull] clip_edge_width std::vector ws_cull_keep; std::vector ws_cull_sxy; + std::vector ws_cull_packed_x; std::vector ws_cull_scale; std::vector ws_cull_angle; std::vector draw_distance_clamps; // [[draw_distance.clamp]] @@ -326,6 +327,7 @@ static int psxrecomp_game_main(int argc, char** argv) { ws_cull_clip_edge_width = PSXRecompV4::ws_cull_clip_edge_width(cfg); ws_cull_keep = cfg.ws_cull_keep_sites; ws_cull_sxy = cfg.ws_cull_sxy_sites; + ws_cull_packed_x = cfg.ws_cull_packed_x_sites; ws_cull_masked_reject = cfg.ws_cull_masked_reject_sites; ws_cull_scale = cfg.ws_cull_scale_sites; ws_cull_angle = cfg.ws_cull_angle_sites; @@ -445,6 +447,7 @@ static int psxrecomp_game_main(int argc, char** argv) { ws_cull_clip_edge_width = PSXRecompV4::ws_cull_clip_edge_width(wscfg); if (ws_cull_keep.empty()) ws_cull_keep = wscfg.ws_cull_keep_sites; if (ws_cull_sxy.empty()) ws_cull_sxy = wscfg.ws_cull_sxy_sites; + if (ws_cull_packed_x.empty()) ws_cull_packed_x = wscfg.ws_cull_packed_x_sites; if (ws_cull_masked_reject.empty()) ws_cull_masked_reject = wscfg.ws_cull_masked_reject_sites; if (ws_cull_scale.empty()) ws_cull_scale = wscfg.ws_cull_scale_sites; if (ws_cull_angle.empty()) ws_cull_angle = wscfg.ws_cull_angle_sites; @@ -1578,6 +1581,7 @@ static int psxrecomp_game_main(int argc, char** argv) { codegen_config.ws_cull_clip_edge_width = ws_cull_w_imms.front(); codegen_config.ws_cull_keep_sites = ws_cull_keep; codegen_config.ws_cull_sxy_sites = ws_cull_sxy; + codegen_config.ws_cull_packed_x_sites = ws_cull_packed_x; codegen_config.ws_cull_scale_sites = ws_cull_scale; codegen_config.ws_cull_angle_sites = ws_cull_angle; codegen_config.draw_distance_clamp_sites = draw_distance_clamps; diff --git a/recompiler/tests/ws_cull_screen_x_hi_codegen_test.cpp b/recompiler/tests/ws_cull_screen_x_hi_codegen_test.cpp index 5ea46ef0..90281503 100644 --- a/recompiler/tests/ws_cull_screen_x_hi_codegen_test.cpp +++ b/recompiler/tests/ws_cull_screen_x_hi_codegen_test.cpp @@ -321,6 +321,27 @@ void runtime_math() { } // namespace +static void packed_x_forms() { + PSXRecomp::CodeGenConfig config; + config.ws_cull_packed_x_sites.push_back({kBase, 0x0079C02Bu}); + const auto emitted = generate_first_instruction(0x0079C02Bu, config); + check(has(emitted, "psx_ws_cull_packed_x_value"), "packed X emits the shared predicate"); + config.overlay_mode = true; + const auto alias = generate_first_instruction(0x0079C02Au, config); + check(!has(alias, "psx_ws_cull_packed_x_value"), "different overlay opcode stays vanilla"); + for (int sx = -32768; sx <= 32767; ++sx) { + const uint32_t x = (uint32_t)sx << 16; + const uint32_t w = 320u << 16; + check(psx_ws_cull_packed_x_value(x, w, 0) == (x < w), "packed X 4:3 identity"); + check(psx_ws_cull_packed_x_value(x, w, 120) == (sx >= -120 && sx < 440), + "packed X covers exactly the expanded horizontal range"); + } + check(psx_ws_cull_packed_x_value(0xFFFF0001u, 320u << 16, 120) == 0, + "nonzero packed low half stays vanilla"); + check(psx_ws_cull_packed_x_value(0xFFFF0000u, 2048u << 16, 120) == 0, + "non-screen width stays vanilla"); +} + int main(int argc, char** argv) { g_self = argv[0]; if (argc == 3 && std::string(argv[1]) == "--exit-probe") { @@ -335,6 +356,7 @@ int main(int argc, char** argv) { main_exe_guard_and_overlay_variants(); declarations_only_when_the_key_is_used(); runtime_math(); + packed_x_forms(); if (g_exit_probe >= 0) return 3; // no call had that probe index if (failures != 0) { diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 08550135..2ab87ff0 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -418,6 +418,10 @@ uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy); extern int g_psx_ws_sxy_cull_n; uint32_t psx_ws_sxy_cull_vertex_reg(uint32_t pc, uint32_t instr, uint32_t (*read_word)(uint32_t phys)); +void gpu_ws_set_packed_x_sites(const uint32_t *addresses, + const uint32_t *expected, int count); +int psx_ws_cull_packed_x_site(uint32_t pc, uint32_t instr, uint32_t x, + uint32_t width, uint32_t *out); uint32_t psx_ws_angle_widen(uint32_t vanilla); int psx_ws_angle_site(uint32_t pc, uint32_t instr, uint32_t *out); /* bound * (half_extent + margin) / half_extent; identity at 4:3. */ diff --git a/runtime/include/ws_cull_edge.h b/runtime/include/ws_cull_edge.h index 0307f1ab..55b47a5d 100644 --- a/runtime/include/ws_cull_edge.h +++ b/runtime/include/ws_cull_edge.h @@ -75,6 +75,20 @@ extern "C" { #endif /* bgez predicate (1 = branch taken = keep) for a signed screen X. */ +/* SLTU on screen X and width shifted into the high half. Only proven packed + * operands widen; original comparison is exact at 4:3 and for other layouts. */ +static inline uint32_t psx_ws_cull_packed_x_value(uint32_t x, uint32_t width, + int32_t m) +{ + if (m <= 0 || ((x | width) & 0xFFFFu) || width == 0u || + (width >> 16) > 1024u) + return x < width ? 1u : 0u; + int32_t sx = (int16_t)(x >> 16); + int64_t offset = (int64_t)sx + m; + int64_t span = (int64_t)(width >> 16) + 2LL * m; + return offset >= 0 && offset < span ? 1u : 0u; +} + static inline int psx_ws_cull_bgez_value(int32_t v, int32_t m) { return v >= -(m > 0 ? m : 0); diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 22179754..7936b7b4 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -2024,8 +2024,10 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, { uint32_t vanilla = (cpu->gpr[rs] < cpu->gpr[rt]) ? 1u : 0u; uint32_t kept = vanilla; - /* Same precedence as the code generator: scale and keep sites first. */ - if (!psx_ws_cull_scale_site(pc, insn, cpu->gpr[rs], cpu->gpr[rt], &kept) && + /* Same precedence as the code generator: packed-X, scale and keep + * sites first. */ + if (!psx_ws_cull_packed_x_site(pc, insn, cpu->gpr[rs], cpu->gpr[rt], &kept) && + !psx_ws_cull_scale_site(pc, insn, cpu->gpr[rs], cpu->gpr[rt], &kept) && !psx_ws_cull_keep_site(pc, insn, vanilla, &kept) && psx_ws_is_cull_screen_x_site(pc)) kept = (uint32_t)psx_ws_cull_sltu_band(cpu->gpr[rs], cpu->gpr[rt]); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 2b1deaac..8c4e51f5 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -1057,6 +1057,29 @@ typedef struct { uint32_t expected; uint32_t result; } WsCullKeepSite; +typedef struct { uint32_t address, expected; } WsPackedXSite; +static WsPackedXSite ws_packed_x_sites[WS_EXPLICIT_CULL_SITES_MAX]; +static int ws_packed_x_n; +void gpu_ws_set_packed_x_sites(const uint32_t *addresses, + const uint32_t *expected, int count) { + if (count < 0) count = 0; + if (count > WS_EXPLICIT_CULL_SITES_MAX) count = WS_EXPLICIT_CULL_SITES_MAX; + ws_packed_x_n = count; + for (int i = 0; i < count; ++i) { + ws_packed_x_sites[i].address = addresses[i] & 0x1FFFFFFFu; + ws_packed_x_sites[i].expected = expected[i]; + } +} +int psx_ws_cull_packed_x_site(uint32_t pc, uint32_t instr, uint32_t x, + uint32_t width, uint32_t *out) { + for (int i = 0; i < ws_packed_x_n; ++i) { + if (ws_packed_x_sites[i].address != (pc & 0x1FFFFFFFu) || + ws_packed_x_sites[i].expected != instr) continue; + if (out) *out = psx_ws_cull_packed_x_value(x, width, psx_ws_x_margin()); + return 1; + } + return 0; +} static WsCullKeepSite ws_cull_keep_sites[WS_EXPLICIT_CULL_SITES_MAX]; static int ws_cull_keep_n = 0; void gpu_ws_set_cull_keep_sites(const uint32_t *addresses, diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 8b00f22d..648e927a 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -18383,6 +18383,15 @@ int main(int argc, char** argv) { vertex_regs.data(), consumer_addresses.data(), consumer_expected.data(), (int)vertices.size()); } + { + std::vector addresses, expected; + for (const auto& site : gc.ws_cull_packed_x_sites) { + addresses.push_back(site.address); + expected.push_back(site.expected); + } + gpu_ws_set_packed_x_sites(addresses.data(), expected.data(), + (int)addresses.size()); + } { std::vector addresses, expected, masks; for (const auto& site : gc.ws_cull_masked_reject_sites) { From 42ddce61b5c8bf7de55f0193d242f0184b42a8a6 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 18:39:43 -0700 Subject: [PATCH 2/6] Preserve wide backdrop composites and guard GTE flag rejection (cherry picked from commit 6b6402e7f076734c06cdc5ba82a9ac2c5adcc916) --- docs/config_schema.md | 7 ++++- recompiler/src/code_generator.cpp | 5 ++++ recompiler/src/config_loader.cpp | 6 +++-- .../tests/ws_cull_edge_codegen_test.cpp | 23 ++++++++++++++++ runtime/src/dirty_ram_interp.c | 1 + runtime/src/gpu.c | 10 ++++--- runtime/tests/test_ws_cull_edge_interp.c | 26 +++++++++++++++++++ 7 files changed, 72 insertions(+), 6 deletions(-) diff --git a/docs/config_schema.md b/docs/config_schema.md index d562ebbb..3b2326b5 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -221,7 +221,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: @@ -238,6 +239,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 ae3c1c51..3a53479b 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -802,6 +802,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 63f504cd..ddf860c6 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -2559,10 +2559,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 a3e6c937..91e3194f 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"); } // ---- [[widescreen.cull.sxy]] ---------------------------------------------- diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 7936b7b4..b50d3f29 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -2193,6 +2193,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 8c4e51f5..caacd284 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -995,6 +995,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) @@ -2655,13 +2656,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) { @@ -2930,7 +2936,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; @@ -3475,7 +3480,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 9222fc63..576667f7 100644 --- a/runtime/tests/test_ws_cull_edge_interp.c +++ b/runtime/tests/test_ws_cull_edge_interp.c @@ -648,6 +648,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; @@ -695,6 +720,7 @@ int main(int argc, char **argv) { test_screen_x_precedence(); test_masked_reject(); test_sxy_fold(); + test_gte_summary_reject(); test_wide_nclip_branches(); if (failures) { fprintf(stderr, "%d check(s) failed\n", failures); From 4fca2d27d8e19f3f2c00f3cfa14d52280ec9d13d Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 21:02:03 -0700 Subject: [PATCH 3/6] Allow explicit world axes for widescreen visibility cones (cherry picked from commit 2c6e0a3a2040e89d6353e83bdd363f024fd44769) --- docs/config_schema.md | 17 +++++++++++----- recompiler/src/config_loader.cpp | 13 ++++++++++++ recompiler/src/config_loader.h | 5 ++++- recompiler/tests/recompiler_patch_test.cpp | 23 ++++++++++++++++++++-- runtime/include/gpu.h | 1 + runtime/include/ws_aspect_cone_math.h | 4 +++- runtime/src/gpu.c | 14 ++++++++++--- runtime/src/main.cpp | 1 + runtime/tests/test_ws_aspect_cone_math.c | 20 +++++++++++++++++++ 9 files changed, 86 insertions(+), 12 deletions(-) diff --git a/docs/config_schema.md b/docs/config_schema.md index 3b2326b5..d9300dbb 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -330,12 +330,13 @@ A model-list or per-child cosine rejection can use a horizontal-only envelope: ```toml [widescreen.cull.aspect_cone] -forward_addr = "0x1F8000E8" # signed Q12 X/Z/Y halfwords +forward_addr = "0x1F8000E8" # three signed Q12 halfwords +forward_offsets = [0, 4, 2] # byte offsets for X/Z/Y; this title stores X/Y/Z object_type_offset = 12 object_reg = 19 x_reg = 16 -z_reg = 17 -y_reg = 18 +z_reg = 18 +y_reg = 17 hysteresis_pixels = 24 queue_reserve = 4 queue_count_addrs = ["0x1F800144", "0x1F800150", "0x1F80015C"] @@ -352,8 +353,8 @@ expected = "0x0082202A" # signed SLT reject predicate cosine_threshold = 856 # required for SLT; Q10 object_reg = 20 # optional per-site register overrides x_reg = 19 -z_reg = 18 -y_reg = 17 +z_reg = 17 +y_reg = 18 queue_guard = false # this lower-level predicate appends to no fixed queue ``` @@ -362,6 +363,12 @@ queue_guard = false # this lower-level predicate appends to no fixed queue - An `SLTI` site derives its Q10 cosine threshold from the immediate unless `cosine_threshold` is given. An `SLT` site requires it explicitly. - A vanilla keep is always preserved. Only a vanilla rejection is retested. +- `x_reg`/`z_reg` describe the world ground plane; `y_reg` is world vertical. + `forward_offsets` maps the corresponding X/Z/Y forward components from the + three halfwords at `forward_addr`. It must be a permutation of `[0, 2, 4]`. + Omitting it preserves the legacy `[0, 2, 4]` order. Match both register and + forward mappings to the actual camera basis; the title's coordinate names + alone are not sufficient evidence. - Horizontal reach follows the current client aspect. Vertical reach, near/far checks, type dispatch, and the game’s queue-capacity branches are unchanged. diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index ddf860c6..2dd20bae 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -246,6 +246,7 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) { h.u32(site.queue_guard ? 1u : 0u); } h.u32(c.ws_aspect_cone.forward_addr); + for (uint32_t value : c.ws_aspect_cone.forward_offsets) h.u32(value); h.u32(c.ws_aspect_cone.object_type_offset); h.u32(c.ws_aspect_cone.object_reg); h.u32(c.ws_aspect_cone.x_reg); @@ -2785,6 +2786,18 @@ GameConfig load_game_config(const fs::path& config_path_in) { out[i] = (uint32_t)values[i]; } }; + if (cone.contains("forward_offsets")) { + load_cone_int_array("forward_offsets", + ws_aspect_cone.forward_offsets); + auto offsets = ws_aspect_cone.forward_offsets; + std::sort(offsets.begin(), offsets.end()); + if (offsets != std::array{0u, 2u, 4u}) + throw std::runtime_error(fmt::format( + "{}: [widescreen.cull.aspect_cone] " + "forward_offsets must be a permutation of [0, 2, 4] " + "for X, Z, Y (Y is world vertical)", + config_path.string())); + } load_cone_hex_array("queue_count_addrs", ws_aspect_cone.queue_count_addrs); load_cone_int_array("queue_capacities", diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index f40de18e..ed018a18 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -197,7 +197,10 @@ struct WidescreenAspectConeSite { struct WidescreenAspectConeConfig { std::vector sites; - uint32_t forward_addr = 0; // three signed Q12 halfwords: X,Z,Y + uint32_t forward_addr = 0; // three signed Q12 halfwords + // Byte offsets for semantic X,Z,Y. Y is world vertical; legacy configs + // stored the forward vector in X,Z,Y order. + std::array forward_offsets{0u, 2u, 4u}; uint32_t object_type_offset = 0; uint32_t object_reg = 0; uint32_t x_reg = 0; diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp index 92385937..5387ed62 100644 --- a/recompiler/tests/recompiler_patch_test.cpp +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -436,7 +436,7 @@ expected = "0x24420155" "expected must be ADDI/ADDIU rt,zero,imm", "parser rejects non-constant terrain-angle instructions"); - const auto aspect_cone = write_config(root, "aspect-cone", R"toml( + const std::string aspect_tables = R"toml( [widescreen.cull.aspect_cone] forward_addr = "0x1F8000E8" object_type_offset = 12 @@ -463,7 +463,8 @@ x_reg = 19 z_reg = 18 y_reg = 17 queue_guard = false -)toml"); +)toml"; + const auto aspect_cone = write_config(root, "aspect-cone", aspect_tables); const auto aspect_config = PSXRecompV4::load_game_config(aspect_cone); check(aspect_config.ws_aspect_cone.sites.size() == 2 && @@ -484,6 +485,8 @@ queue_guard = false !aspect_config.ws_aspect_cone.sites[1].queue_guard && aspect_config.ws_aspect_cone.forward_addr == 0x1F8000E8u && + aspect_config.ws_aspect_cone.forward_offsets == + std::array{0u, 2u, 4u} && aspect_config.ws_aspect_cone.object_reg == 19u && aspect_config.ws_aspect_cone.x_reg == 16u && aspect_config.ws_aspect_cone.z_reg == 17u && @@ -496,6 +499,22 @@ queue_guard = false const auto aspect_hash = PSXRecompV4::overlay_codegen_config_hash( aspect_config); + auto axis_tables = aspect_tables; + axis_tables.insert(axis_tables.find("forward_addr"), + "forward_offsets = [0, 4, 2]\n"); + const auto axis_config = PSXRecompV4::load_game_config( + write_config(root, "aspect-cone-axis-order", axis_tables)); + check(axis_config.ws_aspect_cone.forward_offsets == + std::array{0u, 4u, 2u}, + "parser maps a middle-vertical forward vector explicitly"); + check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash(axis_config), + "forward component order changes overlay cache identity"); + axis_tables.replace(axis_tables.find("[0, 4, 2]"), 9, "[0, 2, 2]"); + const auto bad_axis_config = + write_config(root, "aspect-cone-repeated-axis", axis_tables); + check_throws([&] { (void)PSXRecompV4::load_game_config(bad_axis_config); }, + "forward_offsets must be a permutation", + "parser rejects a duplicated forward component"); auto changed_aspect_config = aspect_config; changed_aspect_config.ws_aspect_cone.hysteresis_pixels++; check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash( diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 2ab87ff0..91f64156 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -375,6 +375,7 @@ void gpu_ws_set_aspect_cone(const uint32_t *addresses, const uint32_t *queue_guards, int nsites, uint32_t forward_addr, + const uint32_t forward_offsets[3], uint32_t object_type_offset, uint32_t hysteresis_pixels, uint32_t queue_reserve, diff --git a/runtime/include/ws_aspect_cone_math.h b/runtime/include/ws_aspect_cone_math.h index abb3adac..56aed293 100644 --- a/runtime/include/ws_aspect_cone_math.h +++ b/runtime/include/ws_aspect_cone_math.h @@ -8,7 +8,9 @@ extern "C" { /* Test whether a camera-relative vector is inside a rectangular view cone. * - * Coordinates use Tomba 2's X,Z,Y ordering. The camera forward vector is Q12. + * Arguments use semantic X,Z,Y ordering: X/Z span the ground plane and Y is + * world vertical. Map the title's stored components to those axes before + * calling. The camera forward vector is Q12. * `threshold` is the game's Q10 cosine threshold (the SLTI immediate after its * normalized dot product). `extent_pixels` expands only the horizontal * half-angle from the vanilla 160-pixel half-width; the vertical half-angle is diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index caacd284..dcd0db21 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -1306,6 +1306,7 @@ typedef struct { static WsAspectConeSite ws_aspect_cone_sites[WS_EXPLICIT_CULL_SITES_MAX]; static int ws_aspect_cone_n = 0; static uint32_t ws_aspect_cone_forward_addr = 0; +static uint32_t ws_aspect_cone_forward_offsets[3] = {0u, 2u, 4u}; static uint32_t ws_aspect_cone_object_type_offset = 0; static uint32_t ws_aspect_cone_hysteresis_pixels = 0; static uint32_t ws_aspect_cone_queue_reserve = 0; @@ -1334,6 +1335,7 @@ void gpu_ws_set_aspect_cone(const uint32_t *addresses, const uint32_t *queue_guards, int nsites, uint32_t forward_addr, + const uint32_t forward_offsets[3], uint32_t object_type_offset, uint32_t hysteresis_pixels, uint32_t queue_reserve, @@ -1362,6 +1364,9 @@ void gpu_ws_set_aspect_cone(const uint32_t *addresses, (uint8_t)(!queue_guards || queue_guards[i] != 0); } ws_aspect_cone_forward_addr = forward_addr; + for (int i = 0; i < 3; i++) + ws_aspect_cone_forward_offsets[i] = + forward_offsets ? forward_offsets[i] : (uint32_t)i * 2u; ws_aspect_cone_object_type_offset = object_type_offset; ws_aspect_cone_hysteresis_pixels = hysteresis_pixels; ws_aspect_cone_queue_reserve = queue_reserve; @@ -1500,11 +1505,14 @@ uint32_t psx_ws_aspect_cone_result(uint32_t site_address, uint32_t vanilla, (total_margin > ws_cull_guard_pixels) ? total_margin - ws_cull_guard_pixels : 0; const int32_t fx = - (int16_t)psx_read_half(ws_aspect_cone_forward_addr + 0u); + (int16_t)psx_read_half(ws_aspect_cone_forward_addr + + ws_aspect_cone_forward_offsets[0]); const int32_t fz = - (int16_t)psx_read_half(ws_aspect_cone_forward_addr + 2u); + (int16_t)psx_read_half(ws_aspect_cone_forward_addr + + ws_aspect_cone_forward_offsets[1]); const int32_t fy = - (int16_t)psx_read_half(ws_aspect_cone_forward_addr + 4u); + (int16_t)psx_read_half(ws_aspect_cone_forward_addr + + ws_aspect_cone_forward_offsets[2]); const uint32_t threshold = site->cosine_threshold; const int in_visible = diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 648e927a..82221319 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -18490,6 +18490,7 @@ int main(int argc, char** argv) { object_regs.data(), x_regs.data(), z_regs.data(), y_regs.data(), queue_guards.data(), (int)addresses.size(), gc.ws_aspect_cone.forward_addr, + gc.ws_aspect_cone.forward_offsets.data(), gc.ws_aspect_cone.object_type_offset, gc.ws_aspect_cone.hysteresis_pixels, gc.ws_aspect_cone.queue_reserve, diff --git a/runtime/tests/test_ws_aspect_cone_math.c b/runtime/tests/test_ws_aspect_cone_math.c index 30ca90a4..40c6dd7b 100644 --- a/runtime/tests/test_ws_aspect_cone_math.c +++ b/runtime/tests/test_ws_aspect_cone_math.c @@ -43,6 +43,26 @@ int main(void) { 1000, 0, -1000, 2896, 0, 2896, cos_q10, 160), "horizontal expansion does not bypass pitched vertical rejection"); + /* Recorded Tomba 2 village camera in stored X,Y,Z order: + * right=(3568,0,-2009), up=(-342,4037,-607), forward=(1980,696,3516). + * Model EF788 delta=(4967,502,1916) lies at the wide right edge. Both + * vectors must be reordered to semantic X,Z,Y before using this helper. */ + check(!psx_ws_aspect_cone_contains( + 4967, 502, 1916, 1980, 696, 3516, 856, 120), + "legacy storage interpretation reproduces the village rejection"); + check(psx_ws_aspect_cone_contains( + 4967, 1916, 502, 1980, 3516, 696, 856, 120), + "mapped village camera accepts its horizontal edge actor"); + check(!psx_ws_aspect_cone_contains( + 4967, 1916, 502, 1980, 3516, 696, 856, 0), + "same actor remains outside the unexpanded cone"); + check(psx_ws_aspect_cone_contains( + 1980, 3516, 696, 1980, 3516, 696, 856, 0), + "mapped pitched village camera accepts its forward axis"); + check(!psx_ws_aspect_cone_contains( + 1296, 2302, 8770, 1980, 3516, 696, 856, 120), + "mapped village camera still rejects a point above its vertical cone"); + check(!psx_ws_aspect_cone_contains( 1000, 0, 0, q12, 0, 0, 0, 0), "invalid zero threshold is inert"); From 0ca9c3a645996fd0b5fd018e662ed41d291bdb6d Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 21:30:20 -0700 Subject: [PATCH 4/6] Preserve legacy overlay keys for default camera component order (cherry picked from commit 5478e5cfd1ea137b1072cc2f7fbbcc2be6e7f66d) --- recompiler/src/config_loader.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 2dd20bae..99741ba2 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -246,7 +246,12 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) { h.u32(site.queue_guard ? 1u : 0u); } h.u32(c.ws_aspect_cone.forward_addr); - for (uint32_t value : c.ws_aspect_cone.forward_offsets) h.u32(value); + // Preserve cache identities for titles using the existing component order. + // A non-default order changes runtime participation and needs a new key. + if (c.ws_aspect_cone.forward_offsets != std::array{0u, 2u, 4u}) { + h.u32(0x41584953u); // AXIS + for (uint32_t value : c.ws_aspect_cone.forward_offsets) h.u32(value); + } h.u32(c.ws_aspect_cone.object_type_offset); h.u32(c.ws_aspect_cone.object_reg); h.u32(c.ws_aspect_cone.x_reg); From 969eaa01ecbad3e876255e46c71a42068dd83298 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 22:02:07 -0700 Subject: [PATCH 5/6] Recover verified thin terrain faces from zero integer NCLIP (cherry picked from commit bffd30fe8109a52ea47b27d62ee0f10698eb70b9) --- runtime/include/gte_nclip_stats.h | 8 +++ runtime/src/gpu.c | 27 ++++++++ runtime/src/gte.cpp | 38 +++++++++++- runtime/tests/gpu_exec_stubs.inc | 3 + runtime/tests/test_gte_register_access.cpp | 72 ++++++++++++++++++++++ runtime/tests/test_ws_cull_edge_interp.c | 3 + 6 files changed, 150 insertions(+), 1 deletion(-) diff --git a/runtime/include/gte_nclip_stats.h b/runtime/include/gte_nclip_stats.h index 2812f5b6..e4b65c2a 100644 --- a/runtime/include/gte_nclip_stats.h +++ b/runtime/include/gte_nclip_stats.h @@ -48,6 +48,14 @@ void gte_nclip_stats_reset(void); * guest just read from MAC0), else the native sign. */ int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc); +/* Opt-in thin-face rescue for exact-word BLEZ consumers. Unlike the separate + * horizontal-saturation policy, this only changes an architectural zero to a + * validated positive winding. Registration is host-side; the existing overlay + * ws_nclip_branch callback and its ABI remain unchanged. */ +void psx_mod_set_native_wide_nclip_zero_sites(const uint32_t *addresses, + const uint32_t *expected, int count); +int gte_nclip_zero_positive(int32_t native_mac0, uint32_t pc); + /* Kill switch for the exact-site consumer (default on): off makes every exact * site test the native sign, for same-build/same-state A/B and diagnosis. */ void gte_nclip_exact_set_enabled(int on); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index dcd0db21..463c6819 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -26,6 +26,7 @@ #include "crash_trace.h" #include "debug_server.h" #include "cpu_state.h" +#include "gte_nclip_stats.h" #include "event_ring.h" #include "psx_video_timing.h" #include "color_lut.h" @@ -1007,8 +1008,25 @@ 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 WsMaskedRejectSite ws_nclip_zero_sites[WS_EXPLICIT_CULL_SITES_MAX]; +static int ws_nclip_zero_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_zero_sites(const uint32_t* addresses, + const uint32_t* expected, int count) { + if (count < 0 || count > WS_EXPLICIT_CULL_SITES_MAX || + (count && (!addresses || !expected))) abort(); + for (int i = 0; i < count; ++i) { + /* Only BLEZ rs,reject is proved here; positive/negative nonzero + * winding and all other branch shapes retain their native result. */ + if ((addresses[i] & 3u) || expected[i] >> 26 != 6u || + ((expected[i] >> 16) & 31u) != 0u || + ((expected[i] >> 21) & 31u) == 0u) abort(); + ws_nclip_zero_sites[i].address = addresses[i] & 0x1FFFFFFFu; + ws_nclip_zero_sites[i].expected = expected[i]; + } + ws_nclip_zero_site_count = count; +} 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(); @@ -1036,6 +1054,15 @@ 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_zero_site_count; ++i) { + if (ws_nclip_zero_sites[i].address != (pc & 0x1FFFFFFFu) || + ws_nclip_zero_sites[i].expected != instr) continue; + if (mac0 == 0 && gte_nclip_zero_positive(mac0, pc)) { + ++ws_nclip_rescues; + return 0; + } + 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; diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 5d60ef36..e1edccfb 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -1358,6 +1358,7 @@ 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 bool s_nclip_last_zero_safe = false; static uint32_t s_nclip_last_generation; static int32_t s_nclip_previous_native; static int8_t s_nclip_previous_precise_sign; @@ -1366,6 +1367,7 @@ 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_last_zero_safe = false; s_nclip_previous_precise_valid = false; } extern "C" void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks, @@ -1439,6 +1441,26 @@ extern "C" int gte_nclip_native_wide_sign(int32_t native_mac0, int* sign) { *sign = s_nclip_last_precise_sign; return 1; } +extern "C" int gte_nclip_zero_positive(int32_t native_mac0, uint32_t pc) { + const bool usable = native_mac0 == 0 && s_nclip_last_native == 0 && + s_nclip_exact_enabled && gpu_ws_precise_nclip_enabled() && + s_nclip_last_precise_valid && s_nclip_last_zero_safe && + s_nclip_last_generation == s_geom_generation && + !s_gte_replay_sandbox && s_speculative_depth == 0 && + !psx_overlay_shadow_diff_active(); + const bool rescue = usable && s_nclip_last_precise_sign > 0; + if (!s_gte_replay_sandbox && s_speculative_depth == 0) { + if (GteNclipSiteStat *e = nclip_find(s_nclip_site, pc, + &GteNclipSiteStat::pc, + &s_nclip_site_dropped)) { + e->evals++; + if (!usable) e->fallbacks++; + else if (rescue) e->flips++; + e->last_frame = (uint32_t)s_frame_count; + } + } + return rescue; +} /* 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 @@ -1504,6 +1526,7 @@ void gte_nclip(GTEState* gte, uint32_t instr) { s_nclip_last_native = out; s_nclip_last_precise_valid = false; s_nclip_last_horizontal_saturated = false; + s_nclip_last_zero_safe = 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; @@ -1526,16 +1549,24 @@ void gte_nclip(GTEState* gte, uint32_t instr) { * 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; + bool checked = true, saturated = false, zero_safe = true; 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_scaled(gte) / 2 && gte->SZ[i + 1] != 0; + zero_safe &= raw_x[i] > -1024 && raw_x[i] < 1023 && + raw_y[i] > -1024 && raw_y[i] < 1023 && + 2u * gte->SZ[i + 1] > gte_h_scaled(gte); 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; + /* A separate, explicitly registered BLEZ policy may recover a thin + * face rounded to zero. Do not extend it to clamp/near-plane cases or + * nonzero architectural winding, and retain the checked FIFO/depth + * provenance used above. */ + s_nclip_last_zero_safe = out == 0 && checked && !saturated && zero_safe; const bool disagree = (cross > 0 && out <= 0) || (cross < 0 && out >= 0); if (disagree) s_nclip_disagreements++; @@ -2382,6 +2413,9 @@ static void gte_export_cpu_state(CPUState* cpu, static void gte_run_command(PSXRecomp::GTE::GTEState* gte, uint32_t cmd) { using namespace PSXRecomp::GTE; + /* A zero-rescue consumer must immediately follow its own NCLIP, not a + * later GTE command that happens to leave another zero in MAC0. */ + s_nclip_last_zero_safe = false; switch (cmd & 0x3Fu) { case 0x01: gte_rtps(gte, cmd); break; case 0x06: gte_nclip(gte, cmd); break; @@ -2670,6 +2704,7 @@ extern "C" uint32_t gte_read_ctrl(CPUState* cpu, uint8_t reg) { extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { if (reg >= 32) return; + PSXRecomp::GTE::s_nclip_last_zero_safe = false; gte_cpu_canonicalize_backing(cpu); switch (reg) { case 7: case 16: case 17: case 18: case 19: @@ -2736,6 +2771,7 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { extern "C" void gte_write_ctrl(CPUState* cpu, uint8_t reg, uint32_t val) { if (reg >= 32) return; + PSXRecomp::GTE::s_nclip_last_zero_safe = false; gte_cpu_canonicalize_backing(cpu); switch (reg) { case 4: case 12: case 20: case 26: case 27: case 29: case 30: diff --git a/runtime/tests/gpu_exec_stubs.inc b/runtime/tests/gpu_exec_stubs.inc index 76716d1f..c30f2e7a 100644 --- a/runtime/tests/gpu_exec_stubs.inc +++ b/runtime/tests/gpu_exec_stubs.inc @@ -460,6 +460,9 @@ int gte_nclip_native_wide_sign(int32_t mac0, int* sign) { int gte_nclip_native_wide_previous_sign(int32_t mac0, int* sign) { (void)mac0; (void)sign; return 0; } +int gte_nclip_zero_positive(int32_t mac0, uint32_t pc) { + (void)mac0; (void)pc; 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_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index c8f1f279..9ae01110 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -68,6 +68,8 @@ extern "C" int gpu_ws_precise_nclip_enabled(void) { return g_test_precise_nclip_enabled; } extern "C" void gpu_pgxp_rederive_enable(void) {} +/* This fixture has ordinary RAM only; no optional GPU DMA arena is allocated. */ +extern "C" int psx_mod_gpu_dma_memory_contains(uint32_t, uint32_t) { return 0; } static int g_test_shadow_diff = 0; extern "C" int psx_overlay_shadow_diff_active(void) { return g_test_shadow_diff; } extern "C" void psx_ws_note_gte_project(int) {} @@ -775,6 +777,7 @@ 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]); + CHECK(!gte_nclip_zero_positive(0, 0x80011008u)); /* 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++) { @@ -807,6 +810,74 @@ int test_saturated_nclip_keeps_architectural_result() { return 0; } +int test_zero_nclip_requires_current_safe_projection() { + CPUState cpu{}; + gte_precision_tracking_set(1); + g_test_precise_nclip_enabled = 1; + pgxp_set_culling(0); + gte_nclip_exact_set_enabled(1); + /* Captured terrain quad801ACC44, first three vertices. Both first vertices + * quantize to (221,45), although their precise face has positive area. */ + const int16_t rt[] = {3518,0,-2096,-375,4029,-629,2061,732,3460}; + for (unsigned i = 0; i < 4; ++i) + cpu.gte_ctrl[i] = (uint16_t)rt[2*i] | ((uint32_t)(uint16_t)rt[2*i+1] << 16); + cpu.gte_ctrl[4] = (uint32_t)(int32_t)rt[8]; + cpu.gte_ctrl[5] = (uint32_t)-4521; cpu.gte_ctrl[6] = 2122; + cpu.gte_ctrl[7] = (uint32_t)-5132; + cpu.gte_ctrl[24] = 160u << 16; cpu.gte_ctrl[25] = 120u << 16; + cpu.gte_ctrl[26] = 350; + const int16_t v[3][3] = {{9970,-1315,6223},{9938,-1315,6172},{9970,-894,6223}}; + auto project = [&](bool reverse = false) { + for (unsigned i = 0; i < 3; ++i) { + unsigned j = reverse ? 2-i : i; + cpu.gte_data[2*i] = (uint16_t)v[j][0] | ((uint32_t)(uint16_t)v[j][1] << 16); + cpu.gte_data[2*i+1] = (uint32_t)(int32_t)v[j][2]; + } + gte_execute(&cpu, 0x00280030u); + gte_execute(&cpu, 6); + }; + const uint32_t site = 0x8013FF14u; + gte_nclip_stats_reset(); + project(); + CHECK(cpu.gte_data[24] == 0); + CHECK(gte_nclip_zero_positive(0, site)); + GteNclipSiteStat stat{}; + CHECK(gte_nclip_site_stats(&stat, 1) == 1 && stat.pc == site && + stat.evals == 1 && stat.flips == 1 && stat.fallbacks == 0); + CHECK(!gte_nclip_zero_positive(-1, site) && !gte_nclip_zero_positive(1, site)); + CHECK(cpu.gte_data[24] == 0); + gte_nclip_exact_set_enabled(0); + CHECK(!gte_nclip_zero_positive(0, site)); + gte_nclip_exact_set_enabled(1); + project(true); + CHECK(cpu.gte_data[24] == 0 && !gte_nclip_zero_positive(0, site)); + project(); + gte_execute(&cpu, 0x2D); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + gte_write_data(&cpu, 24, 0); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + gte_write_ctrl(&cpu, 26, 350); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + cpu.gte_data[17] = 10; + gte_execute(&cpu, 6); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + gte_precision_speculative_begin(); + CHECK(!gte_nclip_zero_positive(0, site)); + gte_precision_speculative_end(); + gte_precision_timeline_invalidate(); + CHECK(!gte_nclip_zero_positive(0, site)); + project(); + gte_test_seed_precise_projection(0, cpu.gte_data[12] ^ 1u, 221 << 16, 45 << 16, 4906); + gte_execute(&cpu, 6); + CHECK(!gte_nclip_zero_positive(0, site)); + g_test_precise_nclip_enabled = 0; + return 0; +} + /* PGXP precise culling (docs/ENHANCEMENTS.md G1.12): NCLIP's MAC0 takes the * exact sign only while geometry correction and culling are armed, only when * the three SXY shadows are believed, and never in a compared pass. */ @@ -1368,6 +1439,7 @@ int main() { 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_zero_nclip_requires_current_safe_projection()) return rc; if (int rc = test_precision_speculative_transaction()) return rc; if (int rc = test_pgxp_probe_does_not_count_geometry_lookup()) return rc; if (int rc = test_preserve_projection_is_shadow_only()) return rc; diff --git a/runtime/tests/test_ws_cull_edge_interp.c b/runtime/tests/test_ws_cull_edge_interp.c index 576667f7..23f0f6b4 100644 --- a/runtime/tests/test_ws_cull_edge_interp.c +++ b/runtime/tests/test_ws_cull_edge_interp.c @@ -81,6 +81,9 @@ 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_zero_positive(int32_t mac0, uint32_t pc) { + (void)mac0; (void)pc; return 0; +} int gte_nclip_native_wide_sign(int32_t mac0, int* sign) { (void)mac0; *sign = precise_sign; return precise_sign_valid; } From e673396353a5a9d8cf0d66e33718edce7a3b416e Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 15:23:56 -0700 Subject: [PATCH 6/6] Refresh OpenBIOS emitter fingerprint for widescreen cull ports Co-Authored-By: Claude Opus 5.5 (1M context) --- generated/OpenBIOS.emitter.sha | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index 230331ac..0edec7c4 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -d887bd825dee12170e8bf22577543114787a3c7f5f2c9bf5e62b14c990561564 +7f5b86aba7d7b043ec5d58425d88701ed25835629523051fc818b9e143db40b3