Allow explicit world axes for widescreen visibility cones

(cherry picked from commit 2c6e0a3a20)
This commit is contained in:
Matthew Stanley
2026-10-08 21:02:03 -07:00
parent 42ddce61b5
commit 4fca2d27d8
9 changed files with 86 additions and 12 deletions
+12 -5
View File
@@ -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.
+13
View File
@@ -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<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",
+4 -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;
+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(
+1
View File
@@ -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,
+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
+11 -3
View File
@@ -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 =
+1
View File
@@ -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,
+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");