From 4fca2d27d8e19f3f2c00f3cfa14d52280ec9d13d Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 8 Oct 2026 21:02:03 -0700 Subject: [PATCH] Allow explicit world axes for widescreen visibility cones (cherry picked from commit 2c6e0a3a2040e89d6353e83bdd363f024fd44769) --- docs/config_schema.md | 17 +++++++++++----- recompiler/src/config_loader.cpp | 13 ++++++++++++ recompiler/src/config_loader.h | 5 ++++- recompiler/tests/recompiler_patch_test.cpp | 23 ++++++++++++++++++++-- runtime/include/gpu.h | 1 + runtime/include/ws_aspect_cone_math.h | 4 +++- runtime/src/gpu.c | 14 ++++++++++--- runtime/src/main.cpp | 1 + runtime/tests/test_ws_aspect_cone_math.c | 20 +++++++++++++++++++ 9 files changed, 86 insertions(+), 12 deletions(-) diff --git a/docs/config_schema.md b/docs/config_schema.md index 3b2326b5..d9300dbb 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -330,12 +330,13 @@ A model-list or per-child cosine rejection can use a horizontal-only envelope: ```toml [widescreen.cull.aspect_cone] -forward_addr = "0x1F8000E8" # signed Q12 X/Z/Y halfwords +forward_addr = "0x1F8000E8" # three signed Q12 halfwords +forward_offsets = [0, 4, 2] # byte offsets for X/Z/Y; this title stores X/Y/Z object_type_offset = 12 object_reg = 19 x_reg = 16 -z_reg = 17 -y_reg = 18 +z_reg = 18 +y_reg = 17 hysteresis_pixels = 24 queue_reserve = 4 queue_count_addrs = ["0x1F800144", "0x1F800150", "0x1F80015C"] @@ -352,8 +353,8 @@ expected = "0x0082202A" # signed SLT reject predicate cosine_threshold = 856 # required for SLT; Q10 object_reg = 20 # optional per-site register overrides x_reg = 19 -z_reg = 18 -y_reg = 17 +z_reg = 17 +y_reg = 18 queue_guard = false # this lower-level predicate appends to no fixed queue ``` @@ -362,6 +363,12 @@ queue_guard = false # this lower-level predicate appends to no fixed queue - An `SLTI` site derives its Q10 cosine threshold from the immediate unless `cosine_threshold` is given. An `SLT` site requires it explicitly. - A vanilla keep is always preserved. Only a vanilla rejection is retested. +- `x_reg`/`z_reg` describe the world ground plane; `y_reg` is world vertical. + `forward_offsets` maps the corresponding X/Z/Y forward components from the + three halfwords at `forward_addr`. It must be a permutation of `[0, 2, 4]`. + Omitting it preserves the legacy `[0, 2, 4]` order. Match both register and + forward mappings to the actual camera basis; the title's coordinate names + alone are not sufficient evidence. - Horizontal reach follows the current client aspect. Vertical reach, near/far checks, type dispatch, and the game’s queue-capacity branches are unchanged. diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index ddf860c6..2dd20bae 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -246,6 +246,7 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) { h.u32(site.queue_guard ? 1u : 0u); } h.u32(c.ws_aspect_cone.forward_addr); + for (uint32_t value : c.ws_aspect_cone.forward_offsets) h.u32(value); h.u32(c.ws_aspect_cone.object_type_offset); h.u32(c.ws_aspect_cone.object_reg); h.u32(c.ws_aspect_cone.x_reg); @@ -2785,6 +2786,18 @@ GameConfig load_game_config(const fs::path& config_path_in) { out[i] = (uint32_t)values[i]; } }; + if (cone.contains("forward_offsets")) { + load_cone_int_array("forward_offsets", + ws_aspect_cone.forward_offsets); + auto offsets = ws_aspect_cone.forward_offsets; + std::sort(offsets.begin(), offsets.end()); + if (offsets != std::array{0u, 2u, 4u}) + throw std::runtime_error(fmt::format( + "{}: [widescreen.cull.aspect_cone] " + "forward_offsets must be a permutation of [0, 2, 4] " + "for X, Z, Y (Y is world vertical)", + config_path.string())); + } load_cone_hex_array("queue_count_addrs", ws_aspect_cone.queue_count_addrs); load_cone_int_array("queue_capacities", diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index f40de18e..ed018a18 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -197,7 +197,10 @@ struct WidescreenAspectConeSite { struct WidescreenAspectConeConfig { std::vector sites; - uint32_t forward_addr = 0; // three signed Q12 halfwords: X,Z,Y + uint32_t forward_addr = 0; // three signed Q12 halfwords + // Byte offsets for semantic X,Z,Y. Y is world vertical; legacy configs + // stored the forward vector in X,Z,Y order. + std::array forward_offsets{0u, 2u, 4u}; uint32_t object_type_offset = 0; uint32_t object_reg = 0; uint32_t x_reg = 0; diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp index 92385937..5387ed62 100644 --- a/recompiler/tests/recompiler_patch_test.cpp +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -436,7 +436,7 @@ expected = "0x24420155" "expected must be ADDI/ADDIU rt,zero,imm", "parser rejects non-constant terrain-angle instructions"); - const auto aspect_cone = write_config(root, "aspect-cone", R"toml( + const std::string aspect_tables = R"toml( [widescreen.cull.aspect_cone] forward_addr = "0x1F8000E8" object_type_offset = 12 @@ -463,7 +463,8 @@ x_reg = 19 z_reg = 18 y_reg = 17 queue_guard = false -)toml"); +)toml"; + const auto aspect_cone = write_config(root, "aspect-cone", aspect_tables); const auto aspect_config = PSXRecompV4::load_game_config(aspect_cone); check(aspect_config.ws_aspect_cone.sites.size() == 2 && @@ -484,6 +485,8 @@ queue_guard = false !aspect_config.ws_aspect_cone.sites[1].queue_guard && aspect_config.ws_aspect_cone.forward_addr == 0x1F8000E8u && + aspect_config.ws_aspect_cone.forward_offsets == + std::array{0u, 2u, 4u} && aspect_config.ws_aspect_cone.object_reg == 19u && aspect_config.ws_aspect_cone.x_reg == 16u && aspect_config.ws_aspect_cone.z_reg == 17u && @@ -496,6 +499,22 @@ queue_guard = false const auto aspect_hash = PSXRecompV4::overlay_codegen_config_hash( aspect_config); + auto axis_tables = aspect_tables; + axis_tables.insert(axis_tables.find("forward_addr"), + "forward_offsets = [0, 4, 2]\n"); + const auto axis_config = PSXRecompV4::load_game_config( + write_config(root, "aspect-cone-axis-order", axis_tables)); + check(axis_config.ws_aspect_cone.forward_offsets == + std::array{0u, 4u, 2u}, + "parser maps a middle-vertical forward vector explicitly"); + check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash(axis_config), + "forward component order changes overlay cache identity"); + axis_tables.replace(axis_tables.find("[0, 4, 2]"), 9, "[0, 2, 2]"); + const auto bad_axis_config = + write_config(root, "aspect-cone-repeated-axis", axis_tables); + check_throws([&] { (void)PSXRecompV4::load_game_config(bad_axis_config); }, + "forward_offsets must be a permutation", + "parser rejects a duplicated forward component"); auto changed_aspect_config = aspect_config; changed_aspect_config.ws_aspect_cone.hysteresis_pixels++; check(aspect_hash != PSXRecompV4::overlay_codegen_config_hash( diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 2ab87ff0..91f64156 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -375,6 +375,7 @@ void gpu_ws_set_aspect_cone(const uint32_t *addresses, const uint32_t *queue_guards, int nsites, uint32_t forward_addr, + const uint32_t forward_offsets[3], uint32_t object_type_offset, uint32_t hysteresis_pixels, uint32_t queue_reserve, diff --git a/runtime/include/ws_aspect_cone_math.h b/runtime/include/ws_aspect_cone_math.h index abb3adac..56aed293 100644 --- a/runtime/include/ws_aspect_cone_math.h +++ b/runtime/include/ws_aspect_cone_math.h @@ -8,7 +8,9 @@ extern "C" { /* Test whether a camera-relative vector is inside a rectangular view cone. * - * Coordinates use Tomba 2's X,Z,Y ordering. The camera forward vector is Q12. + * Arguments use semantic X,Z,Y ordering: X/Z span the ground plane and Y is + * world vertical. Map the title's stored components to those axes before + * calling. The camera forward vector is Q12. * `threshold` is the game's Q10 cosine threshold (the SLTI immediate after its * normalized dot product). `extent_pixels` expands only the horizontal * half-angle from the vanilla 160-pixel half-width; the vertical half-angle is diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index caacd284..dcd0db21 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -1306,6 +1306,7 @@ typedef struct { static WsAspectConeSite ws_aspect_cone_sites[WS_EXPLICIT_CULL_SITES_MAX]; static int ws_aspect_cone_n = 0; static uint32_t ws_aspect_cone_forward_addr = 0; +static uint32_t ws_aspect_cone_forward_offsets[3] = {0u, 2u, 4u}; static uint32_t ws_aspect_cone_object_type_offset = 0; static uint32_t ws_aspect_cone_hysteresis_pixels = 0; static uint32_t ws_aspect_cone_queue_reserve = 0; @@ -1334,6 +1335,7 @@ void gpu_ws_set_aspect_cone(const uint32_t *addresses, const uint32_t *queue_guards, int nsites, uint32_t forward_addr, + const uint32_t forward_offsets[3], uint32_t object_type_offset, uint32_t hysteresis_pixels, uint32_t queue_reserve, @@ -1362,6 +1364,9 @@ void gpu_ws_set_aspect_cone(const uint32_t *addresses, (uint8_t)(!queue_guards || queue_guards[i] != 0); } ws_aspect_cone_forward_addr = forward_addr; + for (int i = 0; i < 3; i++) + ws_aspect_cone_forward_offsets[i] = + forward_offsets ? forward_offsets[i] : (uint32_t)i * 2u; ws_aspect_cone_object_type_offset = object_type_offset; ws_aspect_cone_hysteresis_pixels = hysteresis_pixels; ws_aspect_cone_queue_reserve = queue_reserve; @@ -1500,11 +1505,14 @@ uint32_t psx_ws_aspect_cone_result(uint32_t site_address, uint32_t vanilla, (total_margin > ws_cull_guard_pixels) ? total_margin - ws_cull_guard_pixels : 0; const int32_t fx = - (int16_t)psx_read_half(ws_aspect_cone_forward_addr + 0u); + (int16_t)psx_read_half(ws_aspect_cone_forward_addr + + ws_aspect_cone_forward_offsets[0]); const int32_t fz = - (int16_t)psx_read_half(ws_aspect_cone_forward_addr + 2u); + (int16_t)psx_read_half(ws_aspect_cone_forward_addr + + ws_aspect_cone_forward_offsets[1]); const int32_t fy = - (int16_t)psx_read_half(ws_aspect_cone_forward_addr + 4u); + (int16_t)psx_read_half(ws_aspect_cone_forward_addr + + ws_aspect_cone_forward_offsets[2]); const uint32_t threshold = site->cosine_threshold; const int in_visible = diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 648e927a..82221319 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -18490,6 +18490,7 @@ int main(int argc, char** argv) { object_regs.data(), x_regs.data(), z_regs.data(), y_regs.data(), queue_guards.data(), (int)addresses.size(), gc.ws_aspect_cone.forward_addr, + gc.ws_aspect_cone.forward_offsets.data(), gc.ws_aspect_cone.object_type_offset, gc.ws_aspect_cone.hysteresis_pixels, gc.ws_aspect_cone.queue_reserve, diff --git a/runtime/tests/test_ws_aspect_cone_math.c b/runtime/tests/test_ws_aspect_cone_math.c index 30ca90a4..40c6dd7b 100644 --- a/runtime/tests/test_ws_aspect_cone_math.c +++ b/runtime/tests/test_ws_aspect_cone_math.c @@ -43,6 +43,26 @@ int main(void) { 1000, 0, -1000, 2896, 0, 2896, cos_q10, 160), "horizontal expansion does not bypass pitched vertical rejection"); + /* Recorded Tomba 2 village camera in stored X,Y,Z order: + * right=(3568,0,-2009), up=(-342,4037,-607), forward=(1980,696,3516). + * Model EF788 delta=(4967,502,1916) lies at the wide right edge. Both + * vectors must be reordered to semantic X,Z,Y before using this helper. */ + check(!psx_ws_aspect_cone_contains( + 4967, 502, 1916, 1980, 696, 3516, 856, 120), + "legacy storage interpretation reproduces the village rejection"); + check(psx_ws_aspect_cone_contains( + 4967, 1916, 502, 1980, 3516, 696, 856, 120), + "mapped village camera accepts its horizontal edge actor"); + check(!psx_ws_aspect_cone_contains( + 4967, 1916, 502, 1980, 3516, 696, 856, 0), + "same actor remains outside the unexpanded cone"); + check(psx_ws_aspect_cone_contains( + 1980, 3516, 696, 1980, 3516, 696, 856, 0), + "mapped pitched village camera accepts its forward axis"); + check(!psx_ws_aspect_cone_contains( + 1296, 2302, 8770, 1980, 3516, 696, 856, 120), + "mapped village camera still rejects a point above its vertical cone"); + check(!psx_ws_aspect_cone_contains( 1000, 0, 0, q12, 0, 0, 0, 0), "invalid zero threshold is inert");