mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01: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
@@ -405,14 +405,22 @@ to the local driver's head instead, at one of two seats:
|
||||
- `first_person_seat = "cockpit"`, the default, sits you at the driver's own eyes, behind the
|
||||
steering wheel, at a life-size scale, so the wheel or handlebar is within reach of your hands.
|
||||
The eye is measured once per race from the character's head bone, while the kart drives straight,
|
||||
undamaged and at normal size, and then frozen; until then the bind pose, or the vehicle's authored
|
||||
seat height, stands in. It is kept at least 0.45 m behind the wheel so a long face or a
|
||||
leaned-forward riding pose cannot put it over the controls. The world scale is
|
||||
undamaged and at normal size, and then frozen. Custom models without a separate eye mesh use
|
||||
the animated head with a small upward/forward offset; until calibration completes the bind pose,
|
||||
or the vehicle's authored seat height, stands in. Rejected measurements never alter that fallback.
|
||||
Eight consecutive samples must stay within two units of the first sample, so a slowly moving
|
||||
starting animation cannot qualify just by moving little each frame. The eye is kept at least
|
||||
0.45 m behind the wheel and at most 0.40 m above the neutral hand targets, so long faces and necks
|
||||
do not leave the controls below comfortable reach. The world scale is
|
||||
`cockpit_units_per_meter` (default 100) multiplied by the character's eye height over 100 units,
|
||||
so tall characters sit at a comparable height, and by the player's current size, so a lightning
|
||||
strike or a mega mushroom resizes the view, the wheel and the grab reach together. The seat
|
||||
follows the simulation's position and driving direction, never the animated chassis, so damage
|
||||
spins and tricks do not throw it around.
|
||||
**Recenter view** (including its key binding) and headset reference-space recentering request a
|
||||
fresh cockpit calibration as well as resetting the headset position. The previous seat stays
|
||||
in place until a new stable, neutral measurement is ready; then height and wheel clearance are
|
||||
recalculated together. Recenter while driving straight at normal size to replace a bad initial seat.
|
||||
- `first_person_seat = "custom"` places the head at `first_person_head_up_meters` and its two
|
||||
companions in the kart's own frame, at `first_person_units_per_meter`.
|
||||
|
||||
@@ -476,6 +484,15 @@ only, so the other racers are untouched, and the original values are restored wh
|
||||
stops or the race ends. This is the one place the first-person camera modifies the game rather than
|
||||
only reading it.
|
||||
|
||||
In cockpit view, your Bullet Bill keeps its animated arms but hides the body, eyes and rear
|
||||
exhaust cone, which otherwise fills the view from inside. The runtime resolves the local racer's
|
||||
Killer model and registers its rigid body position arrays with the renderer for that frame.
|
||||
Only draws at that Bullet Bill's own model-view transform are skipped; opponents sharing the
|
||||
model, arm joints, and the separate ground shadow remain visible. The requests are cleared after
|
||||
the frame and when cockpit view stops. This changes rendering only, without modifying game assets
|
||||
or the item's behaviour. `mkw_vr_bullet_bill_tests` and Aurora's `HiddenModelTest` cover body/arm
|
||||
selection, malformed models, instance matching, and both FIFO and raw draw paths.
|
||||
|
||||
One limitation is worth knowing: Mario Kart still culls the scene from its own chase camera, so a
|
||||
wide head turn in first person can reveal the edge of what the game decided to draw. As with the
|
||||
rest of the race instrumentation, the object offsets this reads are specific to the project's
|
||||
|
||||
@@ -342,6 +342,11 @@ void aurora_clear_native_wheel_vertices(void);
|
||||
void aurora_set_native_wheel_vertices(const void* source, const void* replacement, uint32_t size,
|
||||
const float* modelView);
|
||||
uint32_t aurora_native_wheel_draw_count(void);
|
||||
// GX thread, ordered with draws. Hide only rigid draws binding this array at
|
||||
// this instance's model-view transform. Clear at the end of each guest frame.
|
||||
void aurora_clear_hidden_model_arrays(void);
|
||||
void aurora_hide_model_array(const void* source, const float* modelView);
|
||||
uint32_t aurora_hidden_model_draw_count(void);
|
||||
typedef void (*AuroraFrameWorkerWaitCallback)();
|
||||
// Called from the producer thread at bounded intervals while Aurora waits for
|
||||
// the asynchronous frame worker. The callback must not enter Aurora.
|
||||
|
||||
@@ -11,6 +11,23 @@
|
||||
#include "../../gfx/common.hpp"
|
||||
#include "../../gx/fifo.hpp"
|
||||
#include "../../gx/native_wheel.hpp"
|
||||
#include "../../gx/model_visibility.hpp"
|
||||
|
||||
extern "C" void aurora_clear_hidden_model_arrays() {
|
||||
if (aurora::gx::hiddenModelArrays.empty()) return;
|
||||
aurora::gx::fifo::drain();
|
||||
aurora::gx::hiddenModelLastDraws.store(aurora::gx::hiddenModelDraws);
|
||||
aurora::gx::hiddenModelDraws = 0;
|
||||
aurora::gx::hiddenModelArrays.clear();
|
||||
}
|
||||
extern "C" void aurora_hide_model_array(const void* source, const float* modelView) {
|
||||
if (!source || !modelView) return;
|
||||
aurora::gx::fifo::drain();
|
||||
aurora::gx::HiddenModelArray hidden{source, {}};
|
||||
std::memcpy(hidden.modelView.data(), modelView, sizeof(float) * 12);
|
||||
aurora::gx::hiddenModelArrays.push_back(hidden);
|
||||
}
|
||||
extern "C" uint32_t aurora_hidden_model_draw_count() { return aurora::gx::hiddenModelLastDraws.load(); }
|
||||
|
||||
// GX-thread entry points for the VR native steering wheel (native_wheel.hpp).
|
||||
// The runtime posts these in order with the frame's draws.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "dolphin/gx/GXAurora.h"
|
||||
#include "gx.hpp"
|
||||
#include "native_wheel.hpp"
|
||||
#include "model_visibility.hpp"
|
||||
#include "gx_fmt.hpp"
|
||||
#include "pipeline.hpp"
|
||||
#include "shader_info.hpp"
|
||||
@@ -2367,6 +2368,7 @@ bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, ui
|
||||
|
||||
// This entry point bypasses process(), so it owns the renderer lock itself.
|
||||
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
|
||||
if (model_array_hidden()) return true;
|
||||
const gfx::Range vertRange = push_draw_vertices(vertices, vtxCount, vtxSize);
|
||||
const bool interpolationIdentityActive = frame_interpolation_identity_needed();
|
||||
const PnMtxUsage matrixUsage = interpolationIdentityActive ? pn_mtx_usage(vertices, vtxCount, vtxSize) : PnMtxUsage{};
|
||||
@@ -2401,6 +2403,11 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
|
||||
return false;
|
||||
}
|
||||
|
||||
if (model_array_hidden()) {
|
||||
pos += totalVtxBytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Push raw vertex data to buffer
|
||||
const uint8_t* vertices = data + pos;
|
||||
gfx::Range vertRange = push_draw_vertices(vertices, vtxCount, vtxSize);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
#include "gx.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace aurora::gx {
|
||||
struct HiddenModelArray {
|
||||
const void* source = nullptr;
|
||||
std::array<float, 12> modelView{};
|
||||
};
|
||||
inline std::vector<HiddenModelArray> hiddenModelArrays;
|
||||
inline uint32_t hiddenModelDraws = 0;
|
||||
inline std::atomic<uint32_t> hiddenModelLastDraws{0};
|
||||
|
||||
// Called before uploading vertices or merging draws. Matrix-indexed shapes
|
||||
// are deliberately excluded: Bullet Bill's arms have their own animated joints.
|
||||
inline bool model_array_hidden() {
|
||||
if (hiddenModelArrays.empty() || g_gxState.projType != GX_PERSPECTIVE ||
|
||||
(g_gxState.vtxDesc[GX_VA_POS] != GX_INDEX8 && g_gxState.vtxDesc[GX_VA_POS] != GX_INDEX16) ||
|
||||
g_gxState.vtxDesc[GX_VA_PNMTXIDX] != GX_NONE || g_gxState.currentPnMtx >= MaxPnMtx)
|
||||
return false;
|
||||
const auto* matrix = reinterpret_cast<const float*>(&g_gxState.pnMtx[g_gxState.currentPnMtx].pos);
|
||||
for (const auto& hidden : hiddenModelArrays) {
|
||||
if (g_gxState.arrays[GX_VA_POS].data != hidden.source) continue;
|
||||
bool matches = true;
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
uint32_t actual, expected;
|
||||
std::memcpy(&actual, &matrix[i], 4);
|
||||
std::memcpy(&expected, &hidden.modelView[i], 4);
|
||||
if ((actual & 0x7f800000u) == 0x7f800000u || (expected & 0x7f800000u) == 0x7f800000u ||
|
||||
std::abs(matrix[i] - hidden.modelView[i]) > (i % 4 == 3 ? 0.1f : 0.002f)) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches) {
|
||||
++hiddenModelDraws;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace aurora::gx
|
||||
@@ -64,6 +64,7 @@ if (AURORA_ENABLE_GX)
|
||||
eye_pass_plan_test.cpp
|
||||
foveation_test.cpp
|
||||
native_wheel_test.cpp
|
||||
model_visibility_test.cpp
|
||||
cockpit_geometry_test.cpp
|
||||
texture_bind_group_cache_key_test.cpp
|
||||
../lib/gfx/efb_ram_encoder.cpp
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "gx_test_common.hpp"
|
||||
#include "gx/model_visibility.hpp"
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
namespace {
|
||||
class HiddenModelTest : public GXFifoTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
GXFifoTest::SetUp();
|
||||
flush_and_capture();
|
||||
aurora::gx::hiddenModelArrays.clear();
|
||||
aurora::gx::hiddenModelDraws = 0;
|
||||
aurora::gx::hiddenModelLastDraws = 0;
|
||||
auto& state = gxState();
|
||||
state.lastVtxFmt = GX_VTXFMT0;
|
||||
state.lastVtxSize = 1;
|
||||
state.projType = GX_PERSPECTIVE;
|
||||
state.vtxDesc[GX_VA_POS] = GX_INDEX8;
|
||||
state.arrays[GX_VA_POS].data = source.data();
|
||||
state.arrays[GX_VA_POS].size = source.size();
|
||||
state.arrays[GX_VA_POS].stride = 12;
|
||||
std::memcpy(pos(), matrix.data(), sizeof(float) * 12);
|
||||
state.stateDirty = true;
|
||||
aurora_hide_model_array(source.data(), matrix.data());
|
||||
}
|
||||
void TearDown() override { aurora_clear_hidden_model_arrays(); }
|
||||
float* pos() { return reinterpret_cast<float*>(&gxState().pnMtx[0].pos); }
|
||||
void draw() { decode_fifo({static_cast<u8>(GX_TRIANGLES), 0, 3, 0, 1, 2}); }
|
||||
std::array<uint8_t, 36> source{};
|
||||
std::array<float, 12> matrix{1, 0, 0, 10, 0, 1, 0, 20, 0, 0, 1, 30};
|
||||
};
|
||||
|
||||
TEST_F(HiddenModelTest, LocalBodyIsSkippedBeforeVertexUploadAndClearRestoresIt) {
|
||||
draw();
|
||||
EXPECT_TRUE(aurora::gfx::testing::last_pushed_vertices().empty());
|
||||
aurora_clear_hidden_model_arrays();
|
||||
EXPECT_EQ(aurora_hidden_model_draw_count(), 1u);
|
||||
draw();
|
||||
EXPECT_FALSE(aurora::gfx::testing::last_pushed_vertices().empty());
|
||||
}
|
||||
|
||||
TEST_F(HiddenModelTest, OpponentUsingSameArrayIsDrawn) {
|
||||
pos()[3] += 100;
|
||||
draw();
|
||||
EXPECT_FALSE(aurora::gfx::testing::last_pushed_vertices().empty());
|
||||
EXPECT_EQ(aurora::gx::hiddenModelDraws, 0u);
|
||||
}
|
||||
|
||||
TEST_F(HiddenModelTest, ArmsAndIndexedJointsAreKept) {
|
||||
std::array<uint8_t, 36> arms{};
|
||||
gxState().arrays[GX_VA_POS].data = arms.data();
|
||||
EXPECT_FALSE(aurora::gx::model_array_hidden());
|
||||
gxState().arrays[GX_VA_POS].data = source.data();
|
||||
gxState().vtxDesc[GX_VA_PNMTXIDX] = GX_DIRECT;
|
||||
EXPECT_FALSE(aurora::gx::model_array_hidden());
|
||||
}
|
||||
|
||||
TEST_F(HiddenModelTest, DirectPositionsOrthographicAndInvalidMatricesAreKept) {
|
||||
gxState().vtxDesc[GX_VA_POS] = GX_DIRECT;
|
||||
EXPECT_FALSE(aurora::gx::model_array_hidden());
|
||||
gxState().vtxDesc[GX_VA_POS] = GX_INDEX8;
|
||||
gxState().projType = GX_ORTHOGRAPHIC;
|
||||
EXPECT_FALSE(aurora::gx::model_array_hidden());
|
||||
gxState().projType = GX_PERSPECTIVE;
|
||||
pos()[0] = __builtin_nanf("");
|
||||
EXPECT_FALSE(aurora::gx::model_array_hidden());
|
||||
}
|
||||
|
||||
TEST_F(HiddenModelTest, RawBridgeDrawAlsoSkipsLocalBody) {
|
||||
const std::array<uint8_t, 3> vertices{0, 1, 2};
|
||||
ASSERT_TRUE(aurora::gx::fifo::submit_raw_draw(GX_TRIANGLES, GX_VTXFMT0, vertices.data(), 3, vertices.size()));
|
||||
EXPECT_TRUE(aurora::gfx::testing::last_pushed_vertices().empty());
|
||||
EXPECT_EQ(aurora::gx::hiddenModelDraws, 1u);
|
||||
}
|
||||
} // namespace
|
||||
@@ -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