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

+1 -1
View File
@@ -417,7 +417,7 @@ add_test(NAME mkw_vr_first_person_tests COMMAND mkw_vr_first_person_tests)
# native wheel vertices) and hand steering (grab, turn, hand-off to the game),
# ported from heurazy's mario-kart-wii-VR-port, and tracked hands (grasp, bare-hand
# buttons, flick). All header-only.
foreach(test_name mkw_steering_wheel_tests mkw_vr_cockpit_tests mkw_vr_hand_steering_tests mkw_vr_camera_toggle_tests
foreach(test_name mkw_steering_wheel_tests mkw_vr_cockpit_tests mkw_vr_bullet_bill_tests mkw_vr_hand_steering_tests mkw_vr_camera_toggle_tests
mkw_vr_hand_tracking_tests)
string(REGEX REPLACE "^mkw_" "" test_source "${test_name}")
add_executable(${test_name} "${CMAKE_CURRENT_LIST_DIR}/tests/${test_source}.cpp")
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
namespace GxModelVisibility {
// Game thread. These requests are ordered with the frame's GX commands.
void PostClear();
// Hide rigid draws using this array and this instance's model-view matrix.
// Shared models at other transforms and indexed joints are kept visible.
bool PostHiddenArray(uint32_t guestArray, uint32_t size, const float modelView[12]);
uint32_t LastDrawCount();
} // namespace GxModelVisibility
+58
View File
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace mkw::vr {
struct BulletBillBodyArray {
uint32_t offset = 0;
uint32_t size = 0;
};
// kart_killer's body, eye and exhaust shapes are rigid on matrix 0. Its arms
// use other matrices. Select only position arrays used exclusively by rigid
// body shapes, so even a mod sharing positions with an arm keeps those draws.
// Offsets are relative to the MDL0; no names or game assets are embedded here.
inline std::vector<BulletBillBodyArray> ReadBulletBillBodyArrays(const uint8_t* mdl, size_t size) {
if (!mdl || size < 0x40) return {};
const auto contains = [size](size_t at, size_t length) { return at <= size && length <= size - at; };
const auto read32 = [mdl](size_t at) {
return (uint32_t(mdl[at]) << 24) | (uint32_t(mdl[at + 1]) << 16) |
(uint32_t(mdl[at + 2]) << 8) | mdl[at + 3];
};
const auto read16 = [mdl](size_t at) { return (uint32_t(mdl[at]) << 8) | mdl[at + 1]; };
const uint32_t version = read32(8);
if (read32(0) != 0x4d444c30 || read32(4) != size || version < 8 || version > 11) return {};
const size_t positions = read32(0x18), shapes = read32(version >= 10 ? 0x38 : 0x30);
if (!positions || !shapes || !contains(positions, 8) || !contains(shapes, 8)) return {};
const uint32_t positionCount = read32(positions + 4), shapeCount = read32(shapes + 4);
if (positionCount > 64 || shapeCount > 4096 ||
!contains(positions + 8, size_t(positionCount + 1) * 16) ||
!contains(shapes + 8, size_t(shapeCount + 1) * 16)) return {};
std::vector<BulletBillBodyArray> result;
for (uint32_t i = 1; i <= positionCount; ++i) {
const size_t array = positions + size_t(read32(positions + 8 + i * 16 + 12));
if (!contains(array, 0x38)) return {};
const uint32_t id = read32(array + 0x10);
bool body = false, other = false;
for (uint32_t j = 1; j <= shapeCount; ++j) {
const size_t shape = shapes + size_t(read32(shapes + 8 + j * 16 + 12));
if (!contains(shape, 0x60)) return {};
if (read16(shape + 0x48) != id) continue;
const bool rigidBody = read32(shape + 8) == 0 && (read32(shape + 0xc) & 1u) == 0;
body |= rigidBody;
other |= !rigidBody;
}
if (!body || other) continue;
const size_t data = array + size_t(read32(array + 8));
const uint32_t bytes = uint32_t(mdl[array + 0x1d]) * read16(array + 0x1e);
if (!bytes || !contains(data, bytes)) return {};
result.push_back({static_cast<uint32_t>(data), bytes});
}
return result;
}
} // namespace mkw::vr
+62 -17
View File
@@ -206,6 +206,17 @@ inline float EyeBehindControls(float eyeForward, float controlsForward, float un
return std::min(eyeForward, controlsForward - clearance);
}
// Once the eye is pulled behind the controls, a long neck or snout must not
// leave them down at the player's knees. Keep the measured character scale,
// but cap the seated eye at 40 cm above the neutral hand targets.
inline float EyeAboveControls(float eyeHeight, float controlsHeight, float units) noexcept {
if (!detail::IsFiniteFloat(&controlsHeight) || !detail::IsFiniteFloat(&units) || units <= 0.0f) {
return eyeHeight;
}
const float limit = controlsHeight + 0.40f * units;
return limit >= 5.0f ? std::min(eyeHeight, limit) : eyeHeight;
}
// Tall characters sit higher; normalise them to a comfortable perceived cockpit
// height by growing the world scale with the measured eye height.
inline float CharacterCockpitScale(float eyeHeight) noexcept {
@@ -237,6 +248,15 @@ inline bool NeutralPlayerScale(const std::array<float, 3>& scale) noexcept {
return true;
}
inline bool ValidSeatedEye(const std::array<float, 3>& eye) noexcept {
for (const auto& value : eye) {
if (!detail::IsFiniteFloat(&value) || std::abs(value) > 500.0f) {
return false;
}
}
return eye[1] >= 5.0f;
}
// Eye position resources are in the face bone's local coordinates, whose axes
// differ between characters. Transform their centre through the complete bind
// matrix before applying the vehicle-specific driver placement.
@@ -258,12 +278,7 @@ inline bool ComputeDriverEyeFromBounds(const Mtx34& face, const Mtx34& placement
(minimum.z + maximum.z) * 0.5f);
const auto seat = detail::TransformPoint(placement, model.x, model.y, model.z);
const std::array<float, 3> result{seat.x, seat.y, seat.z};
for (const auto& value : result) {
if (!detail::IsFiniteFloat(&value) || std::abs(value) > 500.0f) {
return false;
}
}
if (seat.y < 5.0f) {
if (!ValidSeatedEye(result)) {
return false;
}
eye = result;
@@ -288,29 +303,50 @@ inline bool ComputeSeatedEye(const Mtx34& faceWorld, const Mtx34& bodyWorld, det
const detail::Vec3 delta{world.x - bodyWorld[3], world.y - bodyWorld[7], world.z - bodyWorld[11]};
const std::array<float, 3> result{detail::Dot(bc, delta) / det, detail::Dot(ca, delta) / det,
detail::Dot(ab, delta) / det};
for (const auto& value : result) {
if (!detail::IsFiniteFloat(&value) || std::abs(value) > 500.0f) {
return false;
}
}
if (result[1] < 5.0f) {
if (!ValidSeatedEye(result)) {
return false;
}
eye = result;
return true;
}
// Mods may have no separately named eye geometry. Estimate just above/ahead
// of the evaluated head, in vehicle axes, not the head bone's rotated axes.
// The animated pose includes the character animation's scale and placement;
// adding the driver's placement again would put some mods below the vehicle.
inline bool ComputeDriverEyeFromHead(const Mtx34& headWorld, const Mtx34& bodyWorld,
std::array<float, 3>& eye) noexcept {
std::array<float, 3> head{};
if (!ComputeSeatedEye(headWorld, bodyWorld, {0, 0, 0}, head)) {
return false;
}
head[1] += 8.0f;
head[2] += 8.0f;
if (!ValidSeatedEye(head)) {
return false;
}
eye = head;
return true;
}
// The neutral seated eye, accepted once eight consecutive safe samples agree
// within two units, then frozen until the driver or the race changes.
// within two units of the first sample, then frozen until the driver or the
// race changes, or a recenter asks for another calibration. Keep the previous
// seat while waiting for a safe replacement.
struct SeatedEyeReference {
std::array<float, 3> value{}, candidate{};
unsigned stable = 0;
bool valid = false;
bool recalibrating = false;
void Recalibrate() noexcept {
stable = 0;
recalibrating = true;
}
void Observe(const std::array<float, 3>& sample, bool safe, bool freeze) {
if (freeze && valid) {
if (freeze && valid && !recalibrating) {
return;
}
if (!safe) {
if (!safe || !ValidSeatedEye(sample)) {
stable = 0;
return;
}
@@ -318,11 +354,16 @@ struct SeatedEyeReference {
for (int i = 0; i < 3; ++i) {
delta = std::max(delta, std::abs(sample[i] - candidate[i]));
}
stable = stable && delta < 2.0f ? stable + 1 : 1;
candidate = sample;
if (stable && delta < 2.0f) {
++stable;
} else {
stable = 1;
candidate = sample;
}
if (stable >= 8) {
value = sample;
valid = true;
recalibrating = false;
stable = 8;
}
}
@@ -521,6 +562,10 @@ void MkwVRFirstPersonCommit() noexcept;
// Drops every captured pointer and the held anchor. Call on race entry/exit.
void MkwVRFirstPersonReset() noexcept;
// Thread-safe request; the guest thread remeasures the cockpit on subsequent
// neutral frames. Keeps the current seat until a replacement is ready.
void MkwVRFirstPersonRecenter() noexcept;
// Producer-side read. Thread-safe. A valid anchor is also what marks the mode
// as engaged, and so what selects the first-person world scale: it is invalid
// whenever the mode is off, the race has not produced a usable anchor, or the
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "gx_model_visibility.h"
#include "gx_internal.h"
#include <aurora/aurora.h>
#include <cstring>
namespace GxModelVisibility {
namespace records {
struct HiddenArray {
const void* source;
float modelView[12];
};
void Clear(const uint8_t*, uint32_t) { aurora_clear_hidden_model_arrays(); }
void Hide(const uint8_t* payload, uint32_t size) {
if (size != sizeof(HiddenArray)) return;
HiddenArray array{};
std::memcpy(&array, payload, sizeof(array));
aurora_hide_model_array(array.source, array.modelView);
}
void Post(const void* source, const float modelView[12]) {
if (!GxThread::Enabled()) {
aurora_hide_model_array(source, modelView);
return;
}
HiddenArray array{source, {}};
std::memcpy(array.modelView, modelView, sizeof(array.modelView));
GxThread::detail::PostRecord(&Hide, &array, sizeof(array));
}
} // namespace records
void PostClear() {
if (!GxThread::Enabled()) {
aurora_clear_hidden_model_arrays();
return;
}
GxThread::detail::PostRecord(&records::Clear, nullptr, 0);
}
bool PostHiddenArray(uint32_t guestArray, uint32_t size, const float modelView[12]) {
if (!guestArray || !size || size > 65536 || !modelView) return false;
const void* sdk = GuestToHostPtr(guestArray, size);
const void* cp = GuestToHostPtr(DecodeCpArrayBaseGuestAddress(guestArray), size);
if (sdk) records::Post(sdk, modelView);
if (cp && cp != sdk) records::Post(cp, modelView);
return sdk || cp;
}
uint32_t LastDrawCount() { return aurora_hidden_model_draw_count(); }
} // namespace GxModelVisibility
+2 -1
View File
@@ -1526,7 +1526,8 @@ void DrawVrSettings() {
"Makes where you are sitting right now the centre of the view, and brings "
"the menu screen back upright in front of you. The race view moves in "
"position only, so the horizon stays level and forward is unchanged; use "
"your headset's own recenter to change forward.");
"your headset's own recenter to change forward. In cockpit view it also "
"remeasures the seat when the driver is straight, undamaged and normal size.");
}
ImGui::SameLine();
// Click to arm, then the next key press is captured in HandleEvents.
+130 -16
View File
@@ -3,15 +3,18 @@
#include "vr/mkw_vr_first_person.h"
#include "gx_native_wheel.h"
#include "gx_model_visibility.h"
#include "memory.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include "vr/cockpit_stabilizer.h"
#include "vr/bullet_bill_model.h"
#include "vr/mkw_vr_policy.h"
#include "vr/mkw_vr_player.h"
#include "vr/native_wheel_mesh.h"
#include "vr/openxr_driving.h"
#include <atomic>
#include <cmath>
#include <mutex>
#include <optional>
@@ -102,6 +105,15 @@ constexpr uint32_t kMaxPlayerModels = 32;
// Kart::Link::GetDriverController (0x80590A40) returns accessor+0x14.
constexpr uint32_t kKartAccessorDriverOffset = 0x14u;
// Killer::Activate (0x8059B7B8) sets bit 27 of KartState+0xC through
// accessor+4: the IN_A_BULLET flag, retained throughout the transformation.
constexpr uint32_t kKartAccessorStateOffset = 0x4u;
constexpr uint32_t kKartStateFlagsOffset = 0xCu;
constexpr uint32_t kKartStateInBullet = 1u << 27;
// Kart::Link::GetKiller (0x80591618): accessor+0x60. Killer::__ct
// (0x8059B658) stores the ModelDirector at +0x34 (+0x38 is its shadow).
constexpr uint32_t kKartAccessorKillerOffset = 0x60u;
constexpr uint32_t kKillerModelOffset = 0x34u;
// Kart::Link::GetMovement (0x8059077C) returns accessor+0x28. Movement's
// driving direction at +0x5C excludes damage spin, trick rotation and visual
// pitch/roll; Kart::Movement::SetScale (0x80581720) stores the player's scale
@@ -348,14 +360,17 @@ struct FirstPersonState {
// The vehicle's own wheel would have been animated but the XR side has
// not published its first driving snapshot yet: it shows unturned.
bool waiting_for_driving = false;
uint32_t hidden_bullet_arrays = 0;
} cockpit;
CockpitStabilizer stabilizer{};
uint64_t stabilized_frame = 0;
SeatedEyeReference seated_eye{};
uint32_t seated_driver = 0;
std::optional<float> cockpit_forward;
std::optional<float> cockpit_height;
// Native wheel copies handed to the GX side and not yet dropped.
bool wheel_arrays_posted = false;
bool hidden_arrays_posted = false;
uint32_t native_wheel_body = 0;
uint32_t native_wheel_unmatched = 0;
// The VR wheel is standing in: recent copies have not been taken.
@@ -366,6 +381,7 @@ struct FirstPersonState {
std::mutex g_mutex;
FirstPersonState g_state;
ModelVisibilityState g_visibility;
std::atomic_bool g_recenter_requested{false};
// Walks to the player's ModelsVisibility, or zero when the race is not up.
uint32_t ResolveModelsVisibility(uint32_t accessor) noexcept {
@@ -620,15 +636,17 @@ bool ReadEyeBounds(uint32_t driver, detail::Vec3& minimum, detail::Vec3& maximum
// The seated eye in the vehicle's own frame, in its units. Measured once from
// the driver's head bone while the kart drives straight and undamaged, then
// frozen until the driver or the race changes; the bind pose serves until then.
// frozen until the driver/race changes or a recenter; the bind pose serves
// until the first calibration completes.
bool ReadDriverEye(const KartPoseRead& kart, std::array<float, 3>& eye) noexcept {
const uint32_t driver = LocalDriver(kart.accessor);
if (g_state.seated_driver != driver) {
g_state.seated_driver = driver;
g_state.seated_eye = {};
g_state.cockpit_forward.reset();
g_state.cockpit_height.reset();
}
if (driver != 0 && g_state.seated_eye.valid) {
if (driver != 0 && g_state.seated_eye.valid && !g_state.seated_eye.recalibrating) {
eye = g_state.seated_eye.value;
return true;
}
@@ -637,8 +655,14 @@ bool ReadDriverEye(const KartPoseRead& kart, std::array<float, 3>& eye) noexcept
if (driver == 0 || !ReadGuestPointer(driver + kDriverBonesOffset, bones) ||
!Memory::Contains(bones, kDriverBoneCount * kDriverBoneRecordBytes) ||
!ReadGuestMtx34(driver + kDriverPlacementOffset, placement)) {
g_state.seated_eye.Observe({}, false, true);
if (driver != 0 && g_state.seated_eye.valid) {
eye = g_state.seated_eye.value;
return true;
}
return false;
}
bool observed = false;
try {
for (uint32_t i = 0; i < kDriverBoneCount; ++i) {
uint32_t name = 0, node = 0;
@@ -660,14 +684,12 @@ bool ReadDriverEye(const KartPoseRead& kart, std::array<float, 3>& eye) noexcept
const bool bounds_found = ReadEyeBounds(driver, minimum, maximum);
uint32_t model = 0, ex = 0, scn = 0, palette = 0, mtx_id = 0;
Mtx34 face_world{}, body_world{};
if (bounds_found && ReadGuestPointer(driver + kDriverModelOffset, model) &&
if (ReadGuestPointer(driver + kDriverModelOffset, model) &&
ReadGuestPointer(model + kModelDirectorScnMdlExOffset, ex) && ReadGuestPointer(ex, scn) &&
ReadGuestPointer(scn + kScnMdlWorldMtxArrayOffset, palette) &&
Memory::TryRead32(node + kResNodeMtxIdOffset, mtx_id) && mtx_id < 128 &&
ReadGuestMtx34(palette + mtx_id * 48u, face_world) &&
ReadGuestMtx34(kart.body + kKartBodyMtxOffset, body_world)) {
const detail::Vec3 local_eye{(minimum.x + maximum.x) * 0.5f, (minimum.y + maximum.y) * 0.5f,
(minimum.z + maximum.z) * 0.5f};
uint32_t damage = 0, damage_type = 0;
const DrivingSnapshot driving = OpenXRReadDriving();
// Only a neutral pose may define the seat: straight ahead, at
@@ -677,16 +699,29 @@ bool ReadDriverEye(const KartPoseRead& kart, std::array<float, 3>& eye) noexcept
Memory::TryRead32(damage + kDamageTypeOffset, damage_type) &&
damage_type == UINT32_MAX && std::abs(driving.steering_input) < 0.15f;
std::array<float, 3> measured{};
if (ComputeSeatedEye(face_world, body_world, local_eye, measured)) {
const bool had_reference = g_state.seated_eye.valid;
const detail::Vec3 local_eye{(minimum.x + maximum.x) * 0.5f, (minimum.y + maximum.y) * 0.5f,
(minimum.z + maximum.z) * 0.5f};
const bool measured_eye =
bounds_found ? ComputeSeatedEye(face_world, body_world, local_eye, measured)
: ComputeDriverEyeFromHead(face_world, body_world, measured);
if (measured_eye) {
observed = true;
const bool calibrating = !g_state.seated_eye.valid || g_state.seated_eye.recalibrating;
g_state.seated_eye.Observe(measured, safe, true);
if (!had_reference && g_state.seated_eye.valid) {
if (calibrating && g_state.seated_eye.valid && !g_state.seated_eye.recalibrating) {
g_state.cockpit_forward.reset();
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] cockpit: seated eye calibrated at (" << measured[0]
<< ", " << measured[1] << ", " << measured[2] << ") units"
g_state.cockpit_height.reset();
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] cockpit: seated eye calibrated from "
<< (bounds_found ? "eye geometry" : "animated head")
<< " at (" << measured[0] << ", " << measured[1] << ", "
<< measured[2] << ") units"
<< std::endl;
}
} else {
g_state.seated_eye.Observe({}, false, true);
}
} else {
g_state.seated_eye.Observe({}, false, true);
}
if (g_state.seated_eye.valid) {
eye = g_state.seated_eye.value;
@@ -697,16 +732,25 @@ bool ReadDriverEye(const KartPoseRead& kart, std::array<float, 3>& eye) noexcept
}
// No eye geometry: a point just above and ahead of the head bone.
const std::array<float, 3> point{bind[3], bind[7] + 8.0f, bind[11] + 8.0f};
std::array<float, 3> fallback{};
for (int row = 0; row < 3; ++row) {
eye[row] = placement[row * 4 + 3] + placement[row * 4] * point[0] +
placement[row * 4 + 1] * point[1] + placement[row * 4 + 2] * point[2];
fallback[row] = placement[row * 4 + 3] + placement[row * 4] * point[0] +
placement[row * 4 + 1] * point[1] + placement[row * 4 + 2] * point[2];
}
if (std::abs(eye[0]) < 300.0f && eye[1] > 10.0f && eye[1] < 500.0f && std::abs(eye[2]) < 400.0f) {
if (ValidSeatedEye(fallback)) {
eye = fallback;
return true;
}
}
} catch (const Memory::AccessViolation&) {
}
if (!observed) {
g_state.seated_eye.Observe({}, false, true);
}
if (driver != 0 && g_state.seated_eye.valid) {
eye = g_state.seated_eye.value;
return true;
}
return false;
}
@@ -854,6 +898,43 @@ bool PublishNativeWheelMesh(uint32_t part, const Mtx34& model_view, const Mtx34&
return published;
}
void DropHiddenModelsLocked() noexcept {
if (g_state.hidden_arrays_posted) {
GxModelVisibility::PostClear();
g_state.hidden_arrays_posted = false;
}
}
uint32_t PublishBulletBillBodyLocked(uint32_t accessor, const Mtx34& view) noexcept {
uint32_t state = 0, flags = 0, killer = 0, model = 0, mdl = 0, ex = 0, scn = 0, palette = 0;
Mtx34 body_world{};
if (!ReadGuestPointer(accessor + kKartAccessorStateOffset, state) ||
!Memory::TryRead32(state + kKartStateFlagsOffset, flags) || !(flags & kKartStateInBullet) ||
!ReadGuestPointer(accessor + kKartAccessorKillerOffset, killer) ||
!ReadGuestPointer(killer + kKillerModelOffset, model) ||
!ReadGuestPointer(model + kModelDirectorResMdlOffset, mdl) || !Memory::Contains(mdl, 0x40) ||
!ReadGuestPointer(model + kModelDirectorScnMdlExOffset, ex) || !ReadGuestPointer(ex, scn) ||
!ReadGuestPointer(scn + kScnMdlWorldMtxArrayOffset, palette) || !ReadGuestMtx34(palette, body_world)) {
return 0;
}
uint32_t posted = 0;
try {
const uint32_t size = Memory::Read32(mdl + 4);
if (size > 0x1000000) return 0;
const uint8_t* bytes = Memory::GetPointer(mdl, size);
const auto arrays = ReadBulletBillBodyArrays(bytes, size);
const auto model_view = ComposeMtx(view, body_world);
for (const auto& array : arrays) {
if (GxModelVisibility::PostHiddenArray(mdl + array.offset, array.size, model_view.data())) {
++posted;
g_state.hidden_arrays_posted = true;
}
}
} catch (const Memory::AccessViolation&) {
}
return posted;
}
// At the race draw boundary, before any of the frame's draws: the kart state
// the cockpit seat needs, and the animated copy of the vehicle's own wheel.
void LatchCockpitLocked(uint64_t guest_frame_index) noexcept {
@@ -929,6 +1010,10 @@ void LatchCockpitLocked(uint64_t guest_frame_index) noexcept {
ReadRaceCameraViewMatrix(TryGetCpuContext(), g_state.camera_address, latch.predicted_view, 0.0f);
latch.valid = true;
if (g_state.anchor.valid && g_state.anchor.cockpit && latch.predicted_view_valid) {
latch.hidden_bullet_arrays = PublishBulletBillBodyLocked(kart.accessor, latch.predicted_view);
}
if (g_state.native_wheel_body != kart.body) {
g_state.native_wheel_body = kart.body;
g_state.native_wheel_unmatched = 0;
@@ -987,6 +1072,7 @@ bool ComputeCockpitAnchorLocked(const Mtx34& view_from_world, const KartPoseRead
const float base_units = g_state.cockpit_units_per_meter;
std::array<float, 3> eye{0.0f, 1.1f * base_units, 0.0f};
if (!ReadDriverEye(kart, eye)) {
eye = {0.0f, 1.1f * base_units, 0.0f};
// KartDriverDispParams: the character's seat height and depth here.
uint32_t settings = 0, seat = 0;
try {
@@ -995,8 +1081,10 @@ bool ComputeCockpitAnchorLocked(const Mtx34& view_from_world, const KartPoseRead
const float y = Memory::ReadFloat32(seat), z = Memory::ReadFloat32(seat + 4);
if (detail::IsFiniteFloat(&y) && detail::IsFiniteFloat(&z) && std::abs(y) < 400.0f &&
std::abs(z) < 400.0f) {
eye[1] += y;
eye[2] = z;
const std::array<float, 3> fallback{0.0f, eye[1] + y, z};
if (ValidSeatedEye(fallback)) {
eye = fallback;
}
}
}
} catch (const Memory::AccessViolation&) {
@@ -1025,11 +1113,15 @@ bool ComputeCockpitAnchorLocked(const Mtx34& view_from_world, const KartPoseRead
if (valid && detail::IsFiniteFloat(&center.z)) {
g_state.cockpit_forward =
EyeBehindControls(eye[2], center.z, render_units, std::abs(left[3] - right[3]) * 0.5f);
g_state.cockpit_height = EyeAboveControls(eye[1], center.y, render_units);
}
}
if (g_state.cockpit_forward) {
eye[2] = *g_state.cockpit_forward;
}
if (g_state.cockpit_height) {
eye[1] = *g_state.cockpit_height;
}
for (int axis = 0; axis < 3; ++axis) {
eye[axis] *= scale[axis];
}
@@ -1125,6 +1217,11 @@ void LogCockpitLocked(uint64_t frame, const Mtx34& view_from_world) noexcept {
<< ", native mesh=" << anchor.native_mesh_prepared
<< ", draws animated=" << GxNativeWheel::LastDrawCount() << std::endl;
const auto& latch = g_state.cockpit;
if (latch.hidden_bullet_arrays != 0) {
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] cockpit Bullet Bill: " << latch.hidden_bullet_arrays
<< " body arrays, " << GxModelVisibility::LastDrawCount()
<< " draws hidden; arms retained" << std::endl;
}
if (latch.predicted_view_valid) {
float rotation = 0.0f, translation = 0.0f;
for (int i = 0; i < 12; ++i) {
@@ -1252,6 +1349,10 @@ void MkwVRFirstPersonApplyConfiguredSettings() noexcept {
g_state.hold_frames = 0;
}
g_state.seat = seat;
if (g_state.cockpit_units_per_meter != cockpit_units) {
g_state.cockpit_forward.reset();
g_state.cockpit_height.reset();
}
g_state.cockpit_units_per_meter = cockpit_units;
g_state.steering_wheel = RuntimeConfigFile::VrSteeringWheel();
g_state.native_steering_wheel = RuntimeConfigFile::VrNativeSteeringWheel();
@@ -1282,19 +1383,29 @@ void MkwVRFirstPersonReset() noexcept {
g_state.logged_frame = 0;
DropNativeWheelLocked();
g_state.cockpit = {};
DropHiddenModelsLocked();
g_state.stabilizer = {};
g_state.stabilized_frame = 0;
g_state.seated_eye = {};
g_state.seated_driver = 0;
g_state.cockpit_forward.reset();
g_state.cockpit_height.reset();
g_state.native_wheel_body = 0;
g_state.native_wheel_unmatched = 0;
g_state.native_wheel_fallback = false;
g_state.native_wheel_switch_logs = 0;
}
void MkwVRFirstPersonRecenter() noexcept {
g_recenter_requested.store(true, std::memory_order_release);
}
void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_address) noexcept {
std::lock_guard lock(g_mutex);
DropHiddenModelsLocked();
if (g_recenter_requested.exchange(false, std::memory_order_acq_rel)) {
g_state.seated_eye.Recalibrate();
}
g_state.camera_address = race_camera_address;
if (!g_state.enabled) {
g_state.anchor = {};
@@ -1337,7 +1448,10 @@ void MkwVRFirstPersonCommit() noexcept {
std::lock_guard lock(g_mutex);
// After this frame's draws, whatever the anchor makes of it.
struct FinishWheel {
~FinishWheel() { FinishNativeWheelFrameLocked(); }
~FinishWheel() {
FinishNativeWheelFrameLocked();
DropHiddenModelsLocked();
}
} finish_wheel;
if (!g_state.armed) {
return;
+2
View File
@@ -1529,6 +1529,7 @@ private:
// this frame's head pose rather than the next one's.
void ServiceRecenterRequest() noexcept {
if (recenter_requested_.exchange(false, std::memory_order_acq_rel)) {
MkwVRFirstPersonRecenter();
ResetTrackingOrigin();
}
}
@@ -1799,6 +1800,7 @@ private:
void ApplyPendingReferenceSpaceChange(const OpenXRFrame& frame) noexcept {
if (runtime_->ConsumeAppSpaceChangesThrough(frame.predicted_display_time)) {
MkwVRFirstPersonRecenter();
ResetTrackingOrigin();
}
}
+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);