mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 02:00:14 +02:00
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:
1 parent
8c5a7126da
commit
3c20f677eb
17 files changed
+592
-38
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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(¢er.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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user