Implement Bullet Bill model visibility and cockpit recentering improvements

- Added support for hiding model arrays in the rendering pipeline to optimize performance during VR gameplay.
- Introduced new functions in `gx_model_visibility` to manage hidden model arrays and their visibility based on game state.
- Enhanced cockpit recentering logic to ensure accurate seat measurements and eye positioning during gameplay.
- Updated `FirstPersonState` to track cockpit height and forward direction, improving VR experience.
- Added tests for Bullet Bill model visibility and cockpit height adjustments to ensure functionality and stability.
- Refactored existing code to accommodate new features and improve overall code organization.
This commit is contained in:
iChris4 committed 2026-09-28 23:21:01 +02:00
1 parent 8c5a7126da
commit 3c20f677eb
17 files changed
+592 -38

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "vr/bullet_bill_model.h"
#include <iostream>
#include <vector>
int main() {
// Synthetic MDL0: three rigid body arrays and a fourth on the arm joints.
// No game data is needed to check ownership and offset validation.
std::vector<uint8_t> mdl(0x600);
const auto put32 = [&](size_t at, uint32_t value) {
for (unsigned i = 0; i < 4; ++i) mdl[at + i] = uint8_t(value >> ((3 - i) * 8));
};
const auto put16 = [&](size_t at, uint32_t value) {
mdl[at] = uint8_t(value >> 8);
mdl[at + 1] = uint8_t(value);
};
int failures = 0;
const auto check = [&](bool condition, const char* message) {
if (!condition) { std::cerr << "FAILED: " << message << '\n'; ++failures; }
};
put32(0, 0x4d444c30);
put32(4, uint32_t(mdl.size()));
put32(0x18, 0x40);
put32(0x30, 0xa0);
put32(0x38, 0xa0);
put32(0x44, 4);
put32(0xa4, 4);
for (uint32_t i = 0; i < 4; ++i) {
const uint32_t array = 0x160 + i * 0x40, shape = 0x280 + i * 0x60, data = 0x480 + i * 24;
put32(0x40 + 8 + (i + 1) * 16 + 12, array - 0x40);
put32(array + 8, data - array);
put32(array + 0x10, i);
mdl[array + 0x1d] = 12;
put16(array + 0x1e, 2);
put32(0xa0 + 8 + (i + 1) * 16 + 12, shape - 0xa0);
put32(shape + 8, i == 3 ? UINT32_MAX : 0);
put32(shape + 0xc, i == 3 ? 1 : 0);
put16(shape + 0x48, i);
}
for (uint32_t version : {8u, 9u, 10u, 11u}) {
put32(8, version);
const auto arrays = mkw::vr::ReadBulletBillBodyArrays(mdl.data(), mdl.size());
check(arrays.size() == 3, "body, eyes and cone selected, arms excluded");
for (size_t i = 0; i < arrays.size(); ++i)
check(arrays[i].offset == 0x480 + i * 24 && arrays[i].size == 24, "array data range resolved");
}
put16(0x3a0 + 0x48, 0);
auto shared = mkw::vr::ReadBulletBillBodyArrays(mdl.data(), mdl.size());
check(shared.size() == 2 && shared.front().offset == 0x498, "an array shared with an arm stays visible");
put16(0x3a0 + 0x48, 3);
put32(0x280 + 0xc, 1);
check(mkw::vr::ReadBulletBillBodyArrays(mdl.data(), mdl.size()).size() == 2,
"an indexed matrix shape cannot be assumed to belong to the body");
put32(0x280 + 0xc, 0);
put32(0x160 + 8, UINT32_MAX);
check(mkw::vr::ReadBulletBillBodyArrays(mdl.data(), mdl.size()).empty(), "escaping array offset rejected");
put32(0x160 + 8, 0x480 - 0x160);
put32(0xa0 + 8 + 16 + 12, UINT32_MAX);
check(mkw::vr::ReadBulletBillBodyArrays(mdl.data(), mdl.size()).empty(), "escaping shape offset rejected");
check(mkw::vr::ReadBulletBillBodyArrays(mdl.data(), mdl.size() - 1).empty(), "truncated model rejected");
check(mkw::vr::ReadBulletBillBodyArrays(nullptr, 0).empty(), "missing model rejected");
if (failures) return 1;
std::cout << "Bullet Bill model tests passed\n";
}
+41
View File
@@ -79,6 +79,11 @@ void TestSeatHelpers() {
CheckNear(EyeBehindControls(50.0f, 60.0f, 100.0f, 0.0f), 60.0f - 45.0f, "eye pulled behind the wheel");
CheckNear(EyeBehindControls(50.0f, 60.0f, 100.0f, 50.0f), 60.0f - 55.0f, "a wider wheel keeps more clearance");
CheckNear(EyeBehindControls(-10.0f, 60.0f, 100.0f, 18.0f), -10.0f, "an eye already behind stays put");
CheckNear(EyeAboveControls(69.0f, 37.0f, 100.0f), 69.0f, "normal seated height is unchanged");
CheckNear(EyeAboveControls(126.0f, 58.0f, 126.0f), 108.4f, "long neck does not put the wheel at knee height");
CheckNear(EyeAboveControls(180.0f, 45.0f, 180.0f), 117.0f, "tall driver's reach uses character scale");
CheckNear(EyeAboveControls(69.0f, NAN, 100.0f), 69.0f, "invalid controls leave the eye alone");
CheckNear(EyeAboveControls(69.0f, -100.0f, 100.0f), 69.0f, "bad controls cannot push the eye below the seat");
}
void TestDriverEye() {
@@ -92,6 +97,7 @@ void TestDriverEye() {
Check(!ComputeDriverEyeFromBounds(face, placement, {2, 8, 0}, {-2, 12, 4}, eye), "inverted bounds rejected");
Check(!ComputeDriverEyeFromBounds(Translation(0, -50, 0), placement, {0, 0, 0}, {1, 1, 1}, eye),
"an eye below the seat is rejected");
CheckNear(eye[1], 95.0f, "a rejected measurement cannot corrupt the fallback height");
// The same eye through the animated world matrices: the body's own motion
// must not leak into the seat.
@@ -102,6 +108,17 @@ void TestDriverEye() {
CheckNear(eye[1], 90.0f, "seated eye up", 1e-3f);
CheckNear(eye[2], 15.0f, "seated eye forward", 1e-3f);
// Custom models may have no _eye group and a small/rotated bind head with
// a large negative bike placement. Only the evaluated head includes the
// animation's scale and riding posture. Do not apply placement twice.
Mtx34 customHead{0, 0, 1, 0, 0, -1, 0, 60, 1, 0, 0, 12};
Check(ComputeDriverEyeFromHead(ComposeMtx(body, customHead), body, eye), "custom animated head fallback");
CheckNear(eye[1], 68.0f, "custom head fallback stays above the bike");
CheckNear(eye[2], 20.0f, "fallback offset uses vehicle axes despite rotated head");
Check(!ComputeDriverEyeFromHead(ComposeMtx(body, Translation(0, -20, 0)), body, eye),
"an invalid head is rejected");
CheckNear(eye[1], 68.0f, "failed head fallback leaves caller output intact");
SeatedEyeReference reference;
for (int i = 0; i < 7; ++i) {
reference.Observe({0, 90, 15}, true, true);
@@ -111,6 +128,30 @@ void TestDriverEye() {
Check(reference.valid, "eight stable samples calibrate the seat");
reference.Observe({0, 200, 15}, true, true);
CheckNear(reference.value[1], 90.0f, "a calibrated seat is frozen");
reference.Recalibrate();
reference.Observe({0, 120, 15}, false, true);
Check(reference.valid && reference.recalibrating, "recenter holds the seat until a neutral pose is ready");
CheckNear(reference.value[1], 90.0f, "unsafe recenter keeps old height");
for (int i = 0; i < 7; ++i) {
reference.Observe({0, 75, 15}, true, true);
}
CheckNear(reference.value[1], 90.0f, "partial recalibration keeps old height");
reference.Observe({0, 75, 15}, true, true);
Check(reference.valid && !reference.recalibrating, "recenter accepts fresh stable samples");
CheckNear(reference.value[1], 75.0f, "recenter replaces a bad initial height");
reference.Observe({0, 100, 15}, true, true);
CheckNear(reference.value[1], 75.0f, "replacement freezes again");
SeatedEyeReference drifting;
for (int i = 0; i < 30; ++i) {
drifting.Observe({0, 60.0f + float(i), 15}, true, true);
}
Check(!drifting.valid, "a slowly changing start animation is not a stable seat");
for (int i = 0; i < 7; ++i) {
drifting.Observe({0, 100, 15}, true, true);
}
drifting.Observe({0, NAN, 15}, true, true);
drifting.Observe({0, 100, 15}, true, true);
Check(!drifting.valid, "invalid samples break calibration continuity");
SeatedEyeReference interrupted;
for (int i = 0; i < 5; ++i) {
interrupted.Observe({0, 90, 15}, true, true);