mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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:
+35
-6
@@ -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 @@
|
||||
d887bd825dee12170e8bf22577543114787a3c7f5f2c9bf5e62b14c990561564
|
||||
7f5b86aba7d7b043ec5d58425d88701ed25835629523051fc818b9e143db40b3
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user