Merge pull request #762 from RetroPortingToolKit/integrate/tomba2-resident-to-master-20261024

Land Tomba 2 widescreen cull work on master
This commit is contained in:
Matt Stanley
2026-10-24 16:51:29 -07:00
committed by GitHub
22 changed files with 475 additions and 22 deletions
+35 -6
View File
@@ -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,9 +239,30 @@ 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
`[[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:
@@ -308,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"]
@@ -330,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
```
@@ -340,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.
+1 -1
View File
@@ -1 +1 @@
d887bd825dee12170e8bf22577543114787a3c7f5f2c9bf5e62b14c990561564
7f5b86aba7d7b043ec5d58425d88701ed25835629523051fc818b9e143db40b3
+1
View File
@@ -180,6 +180,7 @@ struct CodeGenConfig {
// [[widescreen.cull.sxy]] packed-SXY AND folds (left edge widened).
std::vector<PSXRecompV4::WidescreenSxyCullSite> ws_cull_sxy_sites;
std::vector<PSXRecompV4::WidescreenMaskedRejectSite> ws_cull_masked_reject_sites;
std::vector<PSXRecompV4::WidescreenPackedXSite> ws_cull_packed_x_sites;
std::vector<PSXRecompV4::WidescreenCullScaleSite> ws_cull_scale_sites;
// Exact `addi[u] rt,zero,imm` 12-bit angular half-extents. The runtime
+26
View File
@@ -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);
}
@@ -1235,6 +1240,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 +3792,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";
+57 -2
View File
@@ -65,6 +65,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;
@@ -235,6 +247,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);
// 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<uint32_t, 3>{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);
@@ -2501,6 +2519,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
std::vector<WidescreenCullKeepSite> ws_cull_keep_sites;
std::vector<WidescreenSxyCullSite> ws_cull_sxy_sites;
std::vector<WidescreenMaskedRejectSite> ws_cull_masked_reject_sites;
std::vector<WidescreenPackedXSite> ws_cull_packed_x_sites;
std::vector<WidescreenCullScaleSite> ws_cull_scale_sites;
std::vector<WidescreenAngleSite> ws_cull_angle_sites;
WidescreenAspectConeConfig ws_aspect_cone;
@@ -2558,10 +2577,12 @@ GameConfig load_game_config(const fs::path& config_path_in) {
"widescreen.cull.masked_reject.expected");
site.reject_mask = parse_hex(toml::find<std::string>(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);
@@ -2569,6 +2590,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<uint32_t> seen;
for (const auto& item : toml::find<toml::array>(cull, "packed_x")) {
WidescreenPackedXSite site;
site.address = parse_hex(toml::find<std::string>(item, "address"),
"widescreen.cull.packed_x.address");
site.expected = parse_hex(toml::find<std::string>(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<uint32_t> seen;
for (const auto& item : toml::find<toml::array>(cull, "keep")) {
@@ -2761,6 +2803,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<uint32_t, 3>{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",
@@ -3109,6 +3163,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);
+11 -1
View File
@@ -197,7 +197,10 @@ struct WidescreenAspectConeSite {
struct WidescreenAspectConeConfig {
std::vector<WidescreenAspectConeSite> 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<uint32_t, 3> forward_offsets{0u, 2u, 4u};
uint32_t object_type_offset = 0;
uint32_t object_reg = 0;
uint32_t x_reg = 0;
@@ -1035,6 +1038,11 @@ struct QualityPreset {
std::vector<std::string> 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;
@@ -1553,6 +1561,8 @@ struct GameConfig {
std::vector<QualityPreset> quality_presets;
// [[mod_immediate.sites]]: run-time-selectable immediates (ModImmediateSite).
std::vector<ModImmediateSite> mod_immediate_sites;
// Guarded SLTU on SX<<16 against screen width<<16; both wide edges.
std::vector<WidescreenPackedXSite> ws_cull_packed_x_sites;
};
// Effective clip_edge_width: explicit value, else screen_w_imms[0], else 320.
+4
View File
@@ -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<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
std::vector<PSXRecompV4::WidescreenSxyCullSite> ws_cull_sxy;
std::vector<PSXRecompV4::WidescreenPackedXSite> ws_cull_packed_x;
std::vector<PSXRecompV4::WidescreenCullScaleSite> ws_cull_scale;
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle;
std::vector<PSXRecompV4::DrawDistanceClampSite> 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;
+21 -2
View File
@@ -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<uint32_t, 3>{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<uint32_t, 3>{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(
@@ -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]] ----------------------------------------------
@@ -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) {
+5
View File
@@ -377,6 +377,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,
@@ -420,6 +421,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. */
+8
View File
@@ -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);
+3 -1
View File
@@ -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
+14
View File
@@ -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);
+5 -2
View File
@@ -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]);
@@ -2191,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);
+68 -6
View File
@@ -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"
@@ -1017,6 +1018,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)
@@ -1028,8 +1030,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();
@@ -1057,6 +1076,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;
@@ -1079,6 +1107,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,
@@ -1304,6 +1355,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;
@@ -1332,6 +1384,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,
@@ -1360,6 +1413,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;
@@ -1498,11 +1554,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 =
@@ -2654,13 +2713,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) {
@@ -2929,7 +2993,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;
@@ -3507,7 +3570,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) {
+37 -1
View File
@@ -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:
+10
View File
@@ -18472,6 +18472,15 @@ int main(int argc, char** argv) {
vertex_regs.data(), consumer_addresses.data(),
consumer_expected.data(), (int)vertices.size());
}
{
std::vector<uint32_t> 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<uint32_t> addresses, expected, masks;
for (const auto& site : gc.ws_cull_masked_reject_sites) {
@@ -18570,6 +18579,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,
+3
View File
@@ -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;
@@ -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;
+20
View File
@@ -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");
+29
View File
@@ -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;
}
@@ -648,6 +651,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 +723,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);