Files
mitch030504--Wiicompiled_VR…/runtime/src/vr/mkw_vr_first_person.cpp
T
iChris4 e7eab8a6b2 Refactor stereo frame worker and interpolation tests for enhanced VR performance
- Updated stereo_frame_worker_smoke.cpp to allow dynamic headset rates and prediction lead time.
- Improved logging to include motion diagnostics and adjusted frame submission logic based on headset frequency.
- Enhanced stereo_interpolation_test.cpp with additional tests for camera motion separation and playback cadence.
- Introduced MkwVRReadSceneView function to read the camera view matrix for improved scene rendering.
- Modified VR first-person logic to support scene view reading and validation.
- Added scene_camera.hpp to encapsulate camera motion handling and inverse view calculations.
- Ensured that the VR integration layer correctly logs motion diagnostics and handles scene playback accurately.
2026-10-01 00:37:49 +02:00

1574 lines
73 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include "vr/mkw_vr_first_person.h"
#include "aurora/aurora.h"
#include "gx_native_wheel.h"
#include "hle/dvd_vr_asset.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>
#include <string>
#include <vector>
extern "C" void func_805A6C58(CpuContext* context);
extern "C" void func_8056A470(CpuContext* context);
extern "C" void func_8056A580(CpuContext* context);
namespace mkw::vr {
namespace {
// ---------------------------------------------------------------------------
// PAL RMCP01 object layout.
//
// Derived from the shipped StaticR.rel and cross-checked against the mkw
// decompilation. Each constant names the accessor that proves it, so a future
// region or a mod that moves these can be re-derived the same way. Keep in
// sync with projects/mkwii/MAP.txt and the generated translations.
// ---------------------------------------------------------------------------
// RaceCamera::GetViewMtx (0x805A6C58) writes the authoritative view matrix to
// its r4 output buffer. The adjacent RaceCamera fields are state vectors, not
// a view matrix, so call the game's getter instead of guessing an object offset.
constexpr uint32_t kRaceCameraScratchBytes = 0x300u;
// GetViewMtx also takes a float argument in f1. It scales the positional offset
// the function folds into the camera it builds, so an inherited garbage value
// puts the view somewhere unrelated to the kart while still looking finite.
// The game's own call site (0x80711198) sources it from *(*(0x809C2898)+0x8BC);
// reproduce that exactly, and fall back to zero, which means "no offset".
constexpr uint32_t kRaceCameraBlendOwnerAddress = 0x809C2898u;
constexpr uint32_t kRaceCameraBlendOffset = 0x8BCu;
// nw4r::g3d::G3DState::GetCameraMtxPtr (0x80064180) resolves the matrix the
// scene is actually rendered with, from a static CameraMtxState: a u16 at +2
// selects the live bank and the 3x4 view matrix sits at +52 within it.
//
// This is the matrix the recorded GX draws carry. RaceCamera::GetViewMtx is
// not: measured on device, the camera it returns sits ~155 units directly
// above the kart, with under 5 units of horizontal separation, so a head
// position derived from it has no chase-camera offset in it at all.
constexpr uint32_t kG3DCameraMtxStateAddress = 0x802BBAB4u;
constexpr uint32_t kG3DCameraMtxBankOffset = 0x2u;
constexpr uint32_t kG3DCameraMtxOffset = 52u;
// Kart::Link::GetKartPosition (0x8059020C) walks proxy -> accessor -> body ->
// physics -> dynamics. The local racer and its accessor are resolved by
// ReadLocalPlayerKart, shared with driver visibility.
constexpr uint32_t kKartAccessorBodyOffset = 0x08u;
constexpr uint32_t kKartBodyPhysicsOffset = 0x90u;
// KartPhysics::pose (Kart::Link::GetMtx 0x80590264). This is the physics-driven
// pose, deliberately not the visual one: an animated frame would bob the
// camera. Kart::Link::GetKartBodyMtx (0x80590278) returns KartBody+0x1C, the
// visual pose, and is the alternative to try if the seat ever looks detached.
constexpr uint32_t kKartPhysicsPoseOffset = 0x9Cu;
// Kart::Link::GetModelsVisibility (0x8059108C) is proxy -> accessor -> +0x58.
// Kart::ModelsVisibility::SetInvisible (0x8056A2F0) is nothing but two stores,
// a u16 at +0x10 and a u8 at +0x12.
//
// Writing them is not enough on its own. UpdateModelsVisibility (0x8056A470)
// is what carries the byte at +0x12 out to the models, looping over them and
// calling a virtual through the secondary vtable at +0x0C, and the game runs it
// during the kart update -- before the draw boundary where this can write. So
// the write has to be followed by running that function again, or nothing ever
// reads it. Measured on device, the mask at +0x10 is already 0 in normal play,
// so it is not the field that decides what draws.
constexpr uint32_t kKartAccessorModelsVisibilityOffset = 0x58u;
constexpr uint32_t kModelsVisibilityMaskOffset = 0x10u;
constexpr uint32_t kModelsVisibilityDrawOffset = 0x12u;
// That one byte reaches every model the loop visits, which is why clearing it
// takes the kart along with the driver. The loop walks an array of models: from
// the holder at *(visibility[0] + 0x14), entries start at +0xD8 with a stride
// of 4 and the count sits at +0xF0. SetModelDraw (0x8056A580) applies the byte
// to a single one of them, so naming an index hides exactly that model.
constexpr uint32_t kModelsVisibilityHolderOffset = 0x14u;
constexpr uint32_t kModelHolderArrayOffset = 0xD8u;
constexpr uint32_t kModelHolderCountOffset = 0xF0u;
constexpr uint32_t kMaxPlayerModels = 32;
// ---------------------------------------------------------------------------
// Cockpit seat and steering wheel, ported from heurazy's mario-kart-wii-VR-port
// (GPL-3.0-or-later). Every offset below is PAL RMCP01 and was re-checked
// against the generated translation (leaf getters) and the mkw decompilation.
// ---------------------------------------------------------------------------
// 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
// (lightning, mega mushroom) at +0x164.
constexpr uint32_t kKartAccessorMovementOffset = 0x28u;
constexpr uint32_t kMovementDirOffset = 0x5Cu;
constexpr uint32_t kMovementScaleOffset = 0x164u;
// Kart::Link::GetDamage (0x80590D20) returns accessor+0x2C; the active damage
// type at +0x1C reads all ones while undamaged.
constexpr uint32_t kKartAccessorDamageOffset = 0x2Cu;
constexpr uint32_t kDamageTypeOffset = 0x1Cu;
// Kart::Link::GetKartPosition (0x8059020C): physics+0x4 -> dynamics, whose
// position is at +0x68.
constexpr uint32_t kKartPhysicsDynamicsOffset = 0x4u;
constexpr uint32_t kDynamicsPositionOffset = 0x68u;
// Kart::Link::IsBike (0x80590A6C) reads accessor+0 -> KartSettings -> +0
// (KartSettings::isBike). KartSettings (0x3C bytes in the decompilation) holds
// KartDriverDispParams* at +0x1C, whose first two floats are the driver's seat
// height and depth on this vehicle.
constexpr uint32_t kKartSettingsIsBikeOffset = 0x0u;
constexpr uint32_t kKartSettingsDriverDispParamsOffset = 0x1Cu;
// Kart::Link::GetKartBodyMtx (0x80590278) returns body+0x1C, the animated body.
constexpr uint32_t kKartBodyMtxOffset = 0x1Cu;
// Body::vf_0x58 (0x8056C500) builds the neutral hand grip frames at +0xA8 and
// +0xD8 from KartDriverDispParams+8 (through 0x80592BF8).
constexpr uint32_t kKartBodyLeftGripOffset = 0xA8u;
constexpr uint32_t kKartBodyRightGripOffset = 0xD8u;
// Kart::BodyBike::__ct (0x8056D858) constructs the BikeHandle at body+0x238 and
// writes its vtable 0x808B5314 at +0xC (Quacker inherits the same handle).
// BodyBike::vf_0x60 (0x8056DA0C) transforms the grip frames by the handle's
// matrix at +0x1C, not by the body's.
constexpr uint32_t kBodyBikeHandleOffset = 0x238u;
constexpr uint32_t kBikeHandleVtableOffset = 0xCu;
constexpr uint32_t kBikeHandleVtable = 0x808B5314u;
constexpr uint32_t kBikeHandleMtxOffset = 0x1Cu;
// A vehicle part's ModelDirector is at +0x7C and a driver's at
// DriverController+0x6C (DriverController::LoadModels 0x807C7828 reads it and
// hands over the placement matrix at +0x78); ModelDirector+0xC is the MDL0.
constexpr uint32_t kPartModelOffset = 0x7Cu;
constexpr uint32_t kDriverModelOffset = 0x6Cu;
constexpr uint32_t kDriverPlacementOffset = 0x78u;
constexpr uint32_t kModelDirectorResMdlOffset = 0xCu;
// DriverController::__ct (0x807C7364) stores the bone table at +0x104, which
// DriverController::GetBoneMatId (0x807D976C) walks: 0x60-byte records, the
// bone's name at +0x14 and its nw4r ResNodeData at +0x18. ResNodeData holds
// mtxId at +0x10 and the bind-pose modelMtx at +0x70.
constexpr uint32_t kDriverBonesOffset = 0x104u;
constexpr uint32_t kDriverBoneRecordBytes = 0x60u;
constexpr uint32_t kDriverBoneCount = 36u;
constexpr uint32_t kDriverBoneNameOffset = 0x14u;
constexpr uint32_t kDriverBoneNodeOffset = 0x18u;
constexpr uint32_t kResNodeMtxIdOffset = 0x10u;
constexpr uint32_t kResNodeModelMtxOffset = 0x70u;
// ModelCalcCallback::GetBoneWorldMtx (0x8055FA90): ModelDirector+0x10 ->
// ScnMdlEx, whose first word is the ScnMdl; ScnMdlSimple::GetScnMtxPos
// (0x80071DC0) returns the world matrix array at +0xEC plus mtxId * 48.
constexpr uint32_t kModelDirectorScnMdlExOffset = 0x10u;
constexpr uint32_t kScnMdlWorldMtxArrayOffset = 0xECu;
// nw4r MDL0 ("MDL0", versions 8-11): the vertex position dictionary's offset
// is at +0x18. ResDic: entry count at +4, 16-byte entries from +8 (entry 0 is
// the root) with name and data offsets at +8/+0xC, relative to the dictionary.
// ResVtxPosData: data offset +8, component count +0x14 (1 = XYZ), type +0x18,
// fraction bits +0x1C, stride +0x1D, count +0x1E, bounds min +0x20, max +0x2C.
constexpr uint32_t kMdl0Magic = 0x4D444C30u;
// Frames of published wheel copies that no draw took before the separate VR
// wheel stands in; it steps aside again as soon as a draw takes one.
constexpr uint32_t kNativeWheelUnmatchedFrames = 30;
// Fallback switches logged per race.
constexpr uint32_t kNativeWheelSwitchLogs = 8;
// Frames the last good anchor survives a failed read before the camera returns
// to the game's own. Rides out a transient null during a respawn or transition
// without letting a genuinely broken anchor persist.
constexpr int kHoldFrames = 10;
// ---------------------------------------------------------------------------
// Guest reads. Everything is bounds-checked and exception-guarded so a pointer
// caught mid-teardown can only cost this frame's anchor.
// ---------------------------------------------------------------------------
bool ReadGuestPointer(uint32_t address, uint32_t& out) noexcept {
return Memory::TryRead32(address, out) && out != 0;
}
constexpr uint32_t kMtx34Bytes = 12u * sizeof(float);
bool ReadGuestMtx34(uint32_t address, Mtx34& out) noexcept {
if (address == 0 || !Memory::Contains(address, kMtx34Bytes)) {
return false;
}
try {
for (uint32_t i = 0; i < out.size(); ++i) {
out[i] = Memory::ReadFloat32(address + i * static_cast<uint32_t>(sizeof(float)));
}
} catch (const Memory::AccessViolation&) {
return false;
}
return detail::IsFiniteMtx34(out);
}
float ReadRaceCameraBlend() noexcept {
uint32_t owner = 0;
if (!ReadGuestPointer(kRaceCameraBlendOwnerAddress, owner) ||
!Memory::Contains(owner + kRaceCameraBlendOffset, sizeof(float))) {
return 0.0f;
}
try {
const float value = Memory::ReadFloat32(owner + kRaceCameraBlendOffset);
return detail::IsFiniteFloat(&value) ? value : 0.0f;
} catch (const Memory::AccessViolation&) {
return 0.0f;
}
}
bool ReadSceneViewMatrix(Mtx34& out) noexcept {
const uint32_t bank_address = kG3DCameraMtxStateAddress + kG3DCameraMtxBankOffset;
if (!Memory::Contains(bank_address, sizeof(uint16_t))) {
return false;
}
try {
const uint32_t bank = Memory::Read16(bank_address);
return ReadGuestMtx34(kG3DCameraMtxStateAddress + bank + kG3DCameraMtxOffset, out);
} catch (const Memory::AccessViolation&) {
return false;
}
}
// `dolly` is GetViewMtx's f1: the game's own call site passes the blend value
// (ReadRaceCameraBlend); zero asks for the camera without that offset.
bool ReadRaceCameraViewMatrix(const CpuContext* context, uint32_t camera_address, Mtx34& out,
float dolly) noexcept {
if (context == nullptr || camera_address == 0 ||
context->gpr[1] < kRaceCameraScratchBytes) {
return false;
}
CpuContext call_context = *context;
const uint32_t scratch = context->gpr[1] - kRaceCameraScratchBytes;
call_context.gpr[3] = camera_address;
call_context.gpr[4] = scratch;
call_context.gpr[5] = scratch + 48u;
// Every argument register has to be set deliberately: the rest of this
// context belongs to the observed function, not to the one being called.
call_context.fpr[1].d = static_cast<double>(dolly);
try {
CpuContextScope scope(&call_context);
func_805A6C58(&call_context);
return ReadGuestMtx34(scratch, out);
} catch (const Memory::AccessViolation&) {
return false;
}
}
// The pointer walk, kept inspectable: on failure `failed_step` names the link
// that broke and the resolved pointers before it are still filled in. One log
// line then says exactly which offset needs revisiting.
struct KartPoseRead : detail::LocalPlayerKartRead {
uint32_t body = 0;
uint32_t physics = 0;
};
KartPoseRead ReadPlayerKartPose(const detail::LocalPlayerKartRead& player, Mtx34& out) noexcept {
KartPoseRead read{player};
if (read.failed_step != nullptr) {
return read;
}
if (!ReadGuestPointer(read.accessor + kKartAccessorBodyOffset, read.body)) {
read.failed_step = "kart body";
} else if (!ReadGuestPointer(read.body + kKartBodyPhysicsOffset, read.physics)) {
read.failed_step = "kart physics";
} else if (!ReadGuestMtx34(read.physics + kKartPhysicsPoseOffset, out)) {
read.failed_step = "kart pose matrix";
}
return read;
}
// ---------------------------------------------------------------------------
// Hiding the player's own models. Like the anchor this only reads the game to
// decide what to write, but unlike the anchor it does modify it, so it owns the
// values it displaced and puts them back when it stops.
struct ModelVisibilityState {
bool hide_driver = false;
// -1 hides every model of the player's kart, the vehicle included; a valid
// index hides only that model. Index 0 is the driver on PAL RMCP01.
int hidden_model = 0;
bool saved = false;
uint32_t saved_object = 0;
uint16_t original_mask = 0;
uint8_t original_draw = 0;
bool logged = false;
bool logged_models = false;
bool logged_range = false;
};
struct FirstPersonState {
bool enabled = false;
FirstPersonHeadOffsets offsets{};
float units_per_meter = RuntimeConfigFile::kVrFirstPersonUnitsPerMeterDefault;
FirstPersonRotation rotation = FirstPersonRotation::YawOnly;
uint32_t camera_address = 0;
detail::LocalPlayerKartRead player_kart{};
HeldItem held_item{};
uint64_t race_generation = 0;
// Armed by the draw boundary, consumed by the frame seal.
bool armed = false;
uint64_t armed_frame = 0;
// The scene matrix as it stood before this frame's draws, kept only to
// report how far it had moved by the time the frame was sealed.
Mtx34 armed_view = kIdentityMtx34;
bool armed_view_valid = false;
FirstPersonAnchor anchor{};
int hold_frames = 0;
bool ever_valid_this_race = false;
bool failure_logged = false;
uint64_t logged_frame = 0;
// Cockpit seat.
FirstPersonSeat seat = FirstPersonSeat::Cockpit;
float cockpit_units_per_meter = RuntimeConfigFile::kVrCockpitUnitsPerMeterDefault;
bool steering_wheel = RuntimeConfigFile::kVrSteeringWheelDefault;
bool native_steering_wheel = RuntimeConfigFile::kVrNativeSteeringWheelDefault;
// [diagnostics] first_person_logging: the once-a-second camera lines below.
bool diagnostic_logging = false;
// Read at the race draw boundary, consumed at the seal.
struct CockpitLatch {
bool valid = false;
uint32_t body = 0;
// The level seat frame: simulation position and driving direction.
Mtx34 stable_body = kIdentityMtx34;
// The frame the seat rides in, and the wheel with it: the level frame
// for "yaw", otherwise the kart's own orientation around the same seat
// position, so the wheel always stays where the view puts it.
Mtx34 seat_body = kIdentityMtx34;
std::array<float, 3> player_scale{1.0f, 1.0f, 1.0f};
Mtx34 body_pose = kIdentityMtx34;
bool grips_valid = false;
Mtx34 left_grip = kIdentityMtx34;
Mtx34 right_grip = kIdentityMtx34;
bool bike = false;
bool handle_valid = false;
Mtx34 handle_pose = kIdentityMtx34;
bool predicted_view_valid = false;
Mtx34 predicted_view = kIdentityMtx34;
bool mesh_published = false;
// 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.
bool native_wheel_fallback = false;
uint32_t native_wheel_switch_logs = 0;
};
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 {
uint32_t visibility = 0;
if (accessor == 0 ||
!ReadGuestPointer(accessor + kKartAccessorModelsVisibilityOffset, visibility)) {
return 0;
}
return visibility;
}
// Carries the visibility fields out to the models, the way the kart update
// does. Without this the fields are just bytes nothing has read.
void ApplyModelsVisibilityToModels(uint32_t visibility) noexcept {
const CpuContext* context = TryGetCpuContext();
if (context == nullptr || visibility == 0) {
return;
}
CpuContext call_context = *context;
call_context.gpr[3] = visibility;
try {
CpuContextScope scope(&call_context);
func_8056A470(&call_context);
} catch (const Memory::AccessViolation&) {
}
}
// Applies the draw byte to one model only. Bounded and pointer-checked because
// this ends in a virtual call on a guest object.
bool ApplyModelDraw(uint32_t visibility, uint32_t holder, uint32_t index) noexcept {
uint32_t model = 0;
if (!ReadGuestPointer(holder + kModelHolderArrayOffset + index * 4u, model)) {
return false;
}
const CpuContext* context = TryGetCpuContext();
if (context == nullptr || !Memory::Contains(model, 4)) {
return false;
}
CpuContext call_context = *context;
call_context.gpr[3] = visibility;
call_context.gpr[4] = model;
try {
CpuContextScope scope(&call_context);
func_8056A580(&call_context);
} catch (const Memory::AccessViolation&) {
return false;
}
return true;
}
// The model array the visibility loop walks, or zero when it cannot be reached.
uint32_t ResolveModelHolder(uint32_t visibility, uint32_t& count) noexcept {
uint32_t holder = 0;
uint32_t owner = 0;
count = 0;
if (!ReadGuestPointer(visibility, owner) ||
!ReadGuestPointer(owner + kModelsVisibilityHolderOffset, holder) ||
!Memory::Contains(holder + kModelHolderCountOffset, 4)) {
return 0;
}
try {
count = Memory::Read32(holder + kModelHolderCountOffset);
} catch (const Memory::AccessViolation&) {
return 0;
}
if (count == 0 || count > kMaxPlayerModels) {
count = 0;
return 0;
}
return holder;
}
void RestoreModelVisibilityLocked() noexcept {
if (!g_visibility.saved) {
return;
}
if (Memory::Contains(g_visibility.saved_object + kModelsVisibilityDrawOffset, 1)) {
try {
Memory::Write16(g_visibility.saved_object + kModelsVisibilityMaskOffset,
g_visibility.original_mask);
Memory::Write8(g_visibility.saved_object + kModelsVisibilityDrawOffset,
g_visibility.original_draw);
ApplyModelsVisibilityToModels(g_visibility.saved_object);
} catch (const Memory::AccessViolation&) {
}
}
g_visibility.saved = false;
g_visibility.saved_object = 0;
}
// Applied at the draw boundary: the kart update has set these for the frame and
// nothing has drawn yet.
void ApplyModelVisibilityLocked() noexcept {
if (!g_visibility.hide_driver) {
RestoreModelVisibilityLocked();
return;
}
const uint32_t visibility = ResolveModelsVisibility(g_state.player_kart.accessor);
if (visibility == 0 ||
!Memory::Contains(visibility + kModelsVisibilityDrawOffset, 1)) {
return;
}
try {
if (!g_visibility.saved || g_visibility.saved_object != visibility) {
RestoreModelVisibilityLocked();
g_visibility.original_mask =
Memory::Read16(visibility + kModelsVisibilityMaskOffset);
g_visibility.original_draw =
Memory::Read8(visibility + kModelsVisibilityDrawOffset);
g_visibility.saved_object = visibility;
g_visibility.saved = true;
if (!g_visibility.logged) {
g_visibility.logged = true;
// The values the game normally holds. If clearing the byte on
// its own does not remove the driver, these say which bits of
// the mask are worth trying instead.
RT_LOG(RT_TAG_RUNTIME)
<< "[mkw-vr] model visibility: object=0x" << std::hex << visibility
<< ", mask=0x" << g_visibility.original_mask << ", driver=0x"
<< static_cast<uint32_t>(g_visibility.original_draw) << std::dec
<< std::endl;
}
}
uint32_t count = 0;
const uint32_t holder = ResolveModelHolder(visibility, count);
if (!g_visibility.logged_models && holder != 0) {
g_visibility.logged_models = true;
RT_LOG(RT_TAG_RUNTIME)
<< "[mkw-vr] model visibility: " << count
<< " models; set first_person_hidden_model to one of 0.." << (count - 1)
<< " to hide a single one, or -1 for all of them" << std::endl;
}
const bool index_in_range =
g_visibility.hidden_model >= 0 && holder != 0 &&
static_cast<uint32_t>(g_visibility.hidden_model) < count;
if (g_visibility.hidden_model >= 0 && !index_in_range) {
// Naming a model the kart does not have should leave it alone, not
// silently fall through to hiding all of them.
if (!g_visibility.logged_range) {
g_visibility.logged_range = true;
RT_LOG(RT_TAG_RUNTIME)
<< "[mkw-vr] model visibility: model " << g_visibility.hidden_model
<< " is out of range for this kart's " << count
<< "; nothing hidden" << std::endl;
}
} else if (index_in_range) {
// Show everything, then take back the one model that is named.
Memory::Write8(visibility + kModelsVisibilityDrawOffset,
g_visibility.original_draw);
ApplyModelsVisibilityToModels(visibility);
Memory::Write8(visibility + kModelsVisibilityDrawOffset, 0);
ApplyModelDraw(visibility, holder,
static_cast<uint32_t>(g_visibility.hidden_model));
Memory::Write8(visibility + kModelsVisibilityDrawOffset,
g_visibility.original_draw);
} else {
Memory::Write8(visibility + kModelsVisibilityDrawOffset, 0);
ApplyModelsVisibilityToModels(visibility);
}
} catch (const Memory::AccessViolation&) {
}
}
// ---------------------------------------------------------------------------
// Cockpit seat and native steering wheel (guest thread, under g_mutex).
// ---------------------------------------------------------------------------
std::array<float, 3> ReadPlayerScale(uint32_t accessor) noexcept {
std::array<float, 3> scale{1.0f, 1.0f, 1.0f};
uint32_t movement = 0;
try {
if (ReadGuestPointer(accessor + kKartAccessorMovementOffset, movement) &&
Memory::Contains(movement + kMovementScaleOffset, 12)) {
for (uint32_t axis = 0; axis < 3; ++axis) {
scale[axis] = ValidPlayerScale(Memory::ReadFloat32(movement + kMovementScaleOffset + axis * 4u));
}
}
} catch (const Memory::AccessViolation&) {
return {1.0f, 1.0f, 1.0f};
}
return scale;
}
uint32_t LocalDriver(uint32_t accessor) noexcept {
uint32_t driver = 0;
if (!ReadGuestPointer(accessor + kKartAccessorDriverOffset, driver) ||
!Memory::Contains(driver, kDriverBonesOffset + 4u)) {
return 0;
}
return driver;
}
bool IsLocalBike(uint32_t accessor) noexcept {
uint32_t settings = 0, is_bike = 0;
return ReadGuestPointer(accessor, settings) && Memory::TryRead32(settings + kKartSettingsIsBikeOffset, is_bike) &&
is_bike != 0;
}
std::string ReadGuestName(uint32_t address, uint32_t limit) {
std::string text;
for (uint32_t n = 0; n < limit && Memory::Contains(address + n, 1); ++n) {
const char c = static_cast<char>(Memory::Read8(address + n));
if (c == '\0') {
break;
}
text += c;
}
return text;
}
// The driver model's "_eye" position array bounds, in the face bone's space.
bool ReadEyeBounds(uint32_t driver, detail::Vec3& minimum, detail::Vec3& maximum) {
uint32_t model = 0, mdl = 0;
if (!ReadGuestPointer(driver + kDriverModelOffset, model) ||
!ReadGuestPointer(model + kModelDirectorResMdlOffset, mdl) || !Memory::Contains(mdl, 0x40) ||
Memory::Read32(mdl) != kMdl0Magic) {
return false;
}
const uint32_t version = Memory::Read32(mdl + 8), offset = Memory::Read32(mdl + 0x18);
if (version < 8 || version > 11 || offset == 0 || offset > 0x100000) {
return false;
}
const uint32_t dic = mdl + offset;
if (!Memory::Contains(dic, 8)) {
return false;
}
const uint32_t count = Memory::Read32(dic + 4);
if (count > 64 || !Memory::Contains(dic, 8 + (count + 1) * 16)) {
return false;
}
for (uint32_t i = 1; i <= count; ++i) {
const uint32_t entry = dic + 8 + i * 16;
const uint32_t name_offset = Memory::Read32(entry + 8), data_offset = Memory::Read32(entry + 12);
if (name_offset > 0x100000 || data_offset > 0x100000) {
continue;
}
if (ReadGuestName(dic + name_offset, 96).find("_eye") == std::string::npos) {
continue;
}
const uint32_t positions = dic + data_offset;
if (!Memory::Contains(positions, 0x38) || Memory::Read32(positions + 0x14) != 1) {
continue;
}
minimum = {Memory::ReadFloat32(positions + 0x20), Memory::ReadFloat32(positions + 0x24),
Memory::ReadFloat32(positions + 0x28)};
maximum = {Memory::ReadFloat32(positions + 0x2C), Memory::ReadFloat32(positions + 0x30),
Memory::ReadFloat32(positions + 0x34)};
return true;
}
return false;
}
// 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/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 && !g_state.seated_eye.recalibrating) {
eye = g_state.seated_eye.value;
return true;
}
uint32_t bones = 0;
Mtx34 placement{};
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;
const uint32_t record = bones + i * kDriverBoneRecordBytes;
if (!ReadGuestPointer(record + kDriverBoneNameOffset, name) ||
!ReadGuestPointer(record + kDriverBoneNodeOffset, node)) {
continue;
}
// PAL DriverMgr's name table (0x808A7288) calls the head bone face_1.
const std::string text = ReadGuestName(name, 32);
if (text != "face_1" && text != "head" && text != "head1" && text != "face") {
continue;
}
Mtx34 bind{};
if (!ReadGuestMtx34(node + kResNodeModelMtxOffset, bind)) {
continue;
}
detail::Vec3 minimum{}, maximum{};
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 (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)) {
uint32_t damage = 0, damage_type = 0;
const DrivingSnapshot driving = OpenXRReadDriving();
// Only a neutral pose may define the seat: straight ahead, at
// normal size and not being knocked about.
const bool safe = NeutralPlayerScale(ReadPlayerScale(kart.accessor)) &&
ReadGuestPointer(kart.accessor + kKartAccessorDamageOffset, damage) &&
Memory::TryRead32(damage + kDamageTypeOffset, damage_type) &&
damage_type == UINT32_MAX && std::abs(driving.steering_input) < 0.15f;
std::array<float, 3> measured{};
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 (calibrating && g_state.seated_eye.valid && !g_state.seated_eye.recalibrating) {
g_state.cockpit_forward.reset();
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;
return true;
}
if (bounds_found && ComputeDriverEyeFromBounds(bind, placement, minimum, maximum, eye)) {
return true;
}
// 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) {
fallback[row] = placement[row * 4 + 3] + placement[row * 4] * point[0] +
placement[row * 4 + 1] * point[1] + placement[row * 4 + 2] * point[2];
}
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;
}
void DropNativeWheelLocked() noexcept {
if (g_state.wheel_arrays_posted) {
GxNativeWheel::PostClear();
g_state.wheel_arrays_posted = false;
}
}
// Decodes a vehicle part's MDL0 position arrays, turns the steering wheel disc
// (or, with `whole_part`, re-seats the whole part) on a copy, and hands each
// changed array to Aurora for the draws carrying `model_view`. The guest's own
// vertices are never written.
bool PublishNativeWheelMesh(uint32_t part, const Mtx34& model_view, const Mtx34& left, const Mtx34& right,
float angle, bool whole_part, const Mtx34& correction) noexcept {
uint32_t model = 0, mdl = 0;
if (!ReadGuestPointer(part + kPartModelOffset, model) ||
!ReadGuestPointer(model + kModelDirectorResMdlOffset, mdl) || !Memory::Contains(mdl, 0x40)) {
return false;
}
const detail::Vec3 center{(left[3] + right[3]) * 0.5f, (left[7] + right[7]) * 0.5f,
(left[11] + right[11]) * 0.5f};
const float radius = std::abs(left[3] - right[3]) * 0.5f;
bool published = false;
try {
if (Memory::Read32(mdl) != kMdl0Magic) {
return false;
}
const uint32_t version = Memory::Read32(mdl + 8);
if (version < 8 || version > 11) {
return false;
}
const uint32_t mdl_size = Memory::Read32(mdl + 4);
const uint8_t* mdl_bytes = mdl_size <= 0x1000000 ? Memory::GetPointer(mdl, mdl_size) : nullptr;
Mtx34 body_from_vertices = kIdentityMtx34;
Mtx34 wheel_model_view = model_view;
if (!whole_part) {
// Body::mtx is the model placement, while the bone GX draws the
// body's node 0 through may have its own authored transform (the
// Baby Booster's rotated root). GX draws its positions through
// placement * bone, so both wheel selection and Aurora's
// local-player matrix match must include it. It is found by node
// id: the Flame Flyer and Cheep Charger list an nw4r_root first.
if (!ReadNativeWheelNodeMatrix(mdl_bytes, mdl_size, 0, body_from_vertices.data())) return false;
wheel_model_view = ComposeMtx(model_view, body_from_vertices);
}
const uint32_t dic_offset = Memory::Read32(mdl + 0x18);
if (dic_offset == 0 || dic_offset > 0x100000) {
return false;
}
const uint32_t dic = mdl + dic_offset;
if (!Memory::Contains(dic, 8)) {
return false;
}
const uint32_t count = Memory::Read32(dic + 4);
if (count > 16 || !Memory::Contains(dic, 8 + (count + 1) * 16)) {
return false;
}
for (uint32_t entry = 1; entry <= count; ++entry) {
const uint32_t offset = Memory::Read32(dic + 8 + entry * 16 + 12);
if (offset > 0x100000) {
continue;
}
const uint32_t header = dic + offset;
if (!Memory::Contains(header, 0x40) || Memory::Read32(header + 0x14) != 1) {
continue;
}
const uint32_t data_offset = Memory::Read32(header + 8), type = Memory::Read32(header + 0x18);
const uint32_t stride = Memory::Read8(header + 0x1D), num = Memory::Read16(header + 0x1E);
const uint32_t component_size = type == 4 ? 4 : (type == 2 || type == 3 ? 2 : 1);
if (type > 4 || stride < 3 * component_size || num > 4096 || data_offset > 0x100000) {
continue;
}
const uint32_t data = header + data_offset, size = num * stride;
if (size == 0 || size > 65536 || !Memory::Contains(data, size)) {
continue;
}
const float scale = std::ldexp(1.0f, -int(Memory::Read8(header + 0x1C)));
std::vector<detail::Vec3> points(num);
bool valid = true;
for (uint32_t i = 0; i < num; ++i) {
for (uint32_t axis = 0; axis < 3; ++axis) {
const uint32_t at = data + i * stride + axis * component_size;
float value = type == 4 ? Memory::ReadFloat32(at)
: type == 3 ? float(int16_t(Memory::Read16(at))) * scale
: type == 2 ? float(Memory::Read16(at)) * scale
: type == 1 ? float(int8_t(Memory::Read8(at))) * scale
: float(Memory::Read8(at)) * scale;
if (!detail::IsFiniteFloat(&value)) {
valid = false;
}
(axis == 0 ? points[i].x : axis == 1 ? points[i].y : points[i].z) = value;
}
}
if (!valid) {
continue;
}
if (whole_part) {
for (auto& point : points) {
point = detail::TransformPoint(correction, point.x, point.y, point.z);
}
} else {
NativeWheelTopology topology(num);
if (!ReadNativeWheelTopology(mdl_bytes, mdl_size, Memory::Read32(header + 0x10), topology) ||
RotateNativeWheelVertices(points, topology, center, radius, angle, &correction,
body_from_vertices) < 8) {
continue;
}
}
const uint8_t* source = Memory::GetPointer(data, size);
if (source == nullptr) {
continue;
}
std::vector<uint8_t> bytes(source, source + size);
for (uint32_t i = 0; i < num && valid; ++i) {
for (uint32_t axis = 0; axis < 3; ++axis) {
const float value = axis == 0 ? points[i].x : axis == 1 ? points[i].y : points[i].z;
uint32_t encoded = 0;
if (type == 4) {
std::memcpy(&encoded, &value, 4);
} else {
const float quantized = std::round(value / scale);
const float low = type == 3 ? -32768.0f : type == 1 ? -128.0f : 0.0f;
const float high = type == 3 ? 32767.0f : type == 2 ? 65535.0f : type == 1 ? 127.0f : 255.0f;
if (!(quantized >= low && quantized <= high)) {
valid = false;
break;
}
encoded = uint32_t(int32_t(quantized));
}
for (uint32_t b = 0; b < component_size; ++b) {
bytes[i * stride + axis * component_size + b] =
uint8_t(encoded >> ((component_size - b - 1) * 8));
}
}
}
if (valid && GxNativeWheel::PostVertices(data, bytes.data(), size, wheel_model_view.data())) {
published = true;
g_state.wheel_arrays_posted = true;
}
}
} catch (const Memory::AccessViolation&) {
}
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 {
auto& latch = g_state.cockpit;
latch = {};
DropNativeWheelLocked();
Mtx34 pose{};
const KartPoseRead kart = ReadPlayerKartPose(g_state.player_kart, pose);
if (kart.failed_step != nullptr) {
return;
}
Mtx34 simulation = pose;
uint32_t damage_type = UINT32_MAX;
try {
// The simulation's position and driving direction, never the animated
// vehicle matrix: damage and tricks spin the chassis, not the seat.
uint32_t dynamics = 0, movement = 0, damage = 0;
if (ReadGuestPointer(kart.physics + kKartPhysicsDynamicsOffset, dynamics) &&
Memory::Contains(dynamics + kDynamicsPositionOffset, 12)) {
for (uint32_t row = 0; row < 3; ++row) {
simulation[row * 4 + 3] = Memory::ReadFloat32(dynamics + kDynamicsPositionOffset + row * 4u);
}
}
if (ReadGuestPointer(kart.accessor + kKartAccessorMovementOffset, movement) &&
Memory::Contains(movement + kMovementDirOffset, 12)) {
const float x = Memory::ReadFloat32(movement + kMovementDirOffset);
const float z = Memory::ReadFloat32(movement + kMovementDirOffset + 8u);
if (detail::IsFiniteFloat(&x) && detail::IsFiniteFloat(&z) && x * x + z * z > 0.01f) {
const float inverse = 1.0f / std::sqrt(x * x + z * z);
simulation[2] = x * inverse;
simulation[10] = z * inverse;
}
}
if (ReadGuestPointer(kart.accessor + kKartAccessorDamageOffset, damage)) {
Memory::TryRead32(damage + kDamageTypeOffset, damage_type);
}
} catch (const Memory::AccessViolation&) {
return;
}
if (!detail::IsFiniteMtx34(simulation) || !ReadGuestMtx34(kart.body + kKartBodyMtxOffset, latch.body_pose)) {
return;
}
const float dt = g_state.stabilized_frame != 0 && guest_frame_index > g_state.stabilized_frame
? float(guest_frame_index - g_state.stabilized_frame) / 60.0f
: 1.0f / 60.0f;
g_state.stabilized_frame = guest_frame_index;
latch.stable_body = g_state.stabilizer.Update(simulation, damage_type != UINT32_MAX, dt);
latch.seat_body = latch.stable_body;
if (g_state.rotation != FirstPersonRotation::YawOnly) {
latch.seat_body = pose;
latch.seat_body[3] = latch.stable_body[3];
latch.seat_body[7] = latch.stable_body[7];
latch.seat_body[11] = latch.stable_body[11];
}
latch.player_scale = ReadPlayerScale(kart.accessor);
latch.body = kart.body;
latch.grips_valid = ReadGuestMtx34(kart.body + kKartBodyLeftGripOffset, latch.left_grip) &&
ReadGuestMtx34(kart.body + kKartBodyRightGripOffset, latch.right_grip);
latch.bike = IsLocalBike(kart.accessor);
if (latch.bike) {
uint32_t vtable = 0;
latch.handle_valid =
Memory::TryRead32(kart.body + kBodyBikeHandleOffset + kBikeHandleVtableOffset, vtable) &&
vtable == kBikeHandleVtable &&
ReadGuestMtx34(kart.body + kBodyBikeHandleOffset + kBikeHandleMtxOffset, latch.handle_pose);
}
// The G3D scene camera is only set once this frame's draws run, so the
// wheel copy is matched against the race camera's own view, asked for with
// no dolly offset; LogCockpitLocked reports how far that is from the
// scene's at the seal.
latch.predicted_view_valid =
g_state.camera_address != 0 &&
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;
g_state.native_wheel_fallback = false;
}
if (!latch.grips_valid || !latch.predicted_view_valid || !g_state.steering_wheel ||
!g_state.native_steering_wheel) {
return;
}
const DrivingSnapshot driving = OpenXRReadDriving();
if (!driving.cockpit_active) {
latch.waiting_for_driving = true;
return;
}
if (latch.bike) {
// BodyBike::vf_0x60 poses the grips by the handle. The whole handle
// part is re-seated on the level cockpit frame so the bars stay with the
// player's hands while the bike banks; the game already turns them.
Mtx34 inverse_body{}, inverse_handle{};
if (!latch.handle_valid || !InvertMtx(latch.body_pose, inverse_body)) {
return;
}
const auto stable_handle = ScaleModelBasis(
ComposeMtx(ComposeMtx(latch.seat_body, inverse_body), latch.handle_pose), latch.player_scale);
const auto rendered_handle = ScaleModelBasis(latch.handle_pose, latch.player_scale);
if (!InvertMtx(rendered_handle, inverse_handle)) {
return;
}
latch.mesh_published =
PublishNativeWheelMesh(kart.body + kBodyBikeHandleOffset, ComposeMtx(latch.predicted_view, rendered_handle),
latch.left_grip, latch.right_grip, driving.visual_angle, true,
ComposeMtx(inverse_handle, stable_handle));
} else {
// Karts bake the wheel into the body: turn just its disc, on the level
// cockpit frame so a spinning chassis does not carry it away.
const auto rendered_body = ScaleModelBasis(latch.body_pose, latch.player_scale);
Mtx34 inverse_rendered{};
if (!InvertMtx(rendered_body, inverse_rendered)) {
return;
}
latch.mesh_published = PublishNativeWheelMesh(
kart.body, ComposeMtx(latch.predicted_view, rendered_body), latch.left_grip, latch.right_grip,
driving.visual_angle, false,
ComposeMtx(inverse_rendered, ScaleModelBasis(latch.seat_body, latch.player_scale)));
}
}
// At the seal, from the scene camera the frame was drawn with.
bool ComputeCockpitAnchorLocked(const Mtx34& view_from_world, const KartPoseRead& kart, const Mtx34& pose,
FirstPersonAnchor& out, const char*& failed_step) noexcept {
const auto& latch = g_state.cockpit;
if (!latch.valid || latch.body != kart.body) {
failed_step = "cockpit seat (the vehicle was not read at the race draw boundary)";
return false;
}
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 {
if (ReadGuestPointer(kart.accessor, settings) &&
ReadGuestPointer(settings + kKartSettingsDriverDispParamsOffset, seat) && Memory::Contains(seat, 8)) {
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) {
const std::array<float, 3> fallback{0.0f, eye[1] + y, z};
if (ValidSeatedEye(fallback)) {
eye = fallback;
}
}
}
} catch (const Memory::AccessViolation&) {
}
}
float render_units = base_units * CharacterCockpitScale(eye[1]);
const auto& scale = latch.player_scale;
// Keep the controls ahead of the seated player even when a long face or a
// leaned-forward riding animation puts its eye point over them.
if (latch.grips_valid && !g_state.cockpit_forward) {
const auto& left = latch.left_grip;
const auto& right = latch.right_grip;
detail::Vec3 center{(left[3] + right[3]) * 0.5f, (left[7] + right[7]) * 0.5f, (left[11] + right[11]) * 0.5f};
bool valid = true;
if (latch.bike) {
Mtx34 inverse_body{};
valid = latch.handle_valid && InvertMtx(latch.body_pose, inverse_body);
if (valid) {
auto local_handle = ComposeMtx(inverse_body, latch.handle_pose);
for (int row = 0; row < 3; ++row) {
local_handle[row * 4 + 3] /= scale[row];
}
center = detail::TransformPoint(local_handle, center.x, center.y, center.z);
}
}
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];
}
render_units *= scale[1];
// "yaw" takes the kart's own driving direction from the level seat frame,
// rather than the chase camera's lagging heading; the other modes take the
// kart's orientation around the same seat (LatchCockpitLocked).
(void)pose;
const FirstPersonRotation rotation =
g_state.rotation == FirstPersonRotation::YawOnly ? FirstPersonRotation::YawPitch : g_state.rotation;
Mtx34 anchor{};
if (!ComputeFirstPersonAnchor(view_from_world, latch.seat_body, eye[0], eye[1], eye[2], rotation, anchor)) {
failed_step = "cockpit anchor math (degenerate camera or kart frame)";
return false;
}
out = {};
out.anchor_from_scene = anchor;
out.valid = true;
out.cockpit = true;
out.units_per_meter = render_units;
out.vehicle_identity = kart.body;
out.bike = latch.bike;
if (latch.grips_valid) {
const detail::Vec3 left{latch.left_grip[3], latch.left_grip[7], latch.left_grip[11]};
const detail::Vec3 right{latch.right_grip[3], latch.right_grip[7], latch.right_grip[11]};
const auto seat_from_world = ComposeMtx(anchor, view_from_world);
const auto seat_from_body = ComposeMtx(seat_from_world, ScaleModelBasis(latch.seat_body, scale));
if (!latch.bike) {
out.native_wheel = ComputeNativeWheelGeometry(seat_from_body, left, right, render_units);
} else if (latch.handle_valid) {
Mtx34 inverse_body{};
if (InvertMtx(latch.body_pose, inverse_body)) {
const auto stable_handle = ScaleModelBasis(
ComposeMtx(ComposeMtx(latch.seat_body, inverse_body), latch.handle_pose), scale);
out.native_wheel = ComputeNativeHandlebarGeometry(ComposeMtx(seat_from_world, stable_handle),
seat_from_body, left, right, render_units);
}
}
}
// Waiting counts as prepared, so the separate VR wheel does not flash up for
// the frame or two the XR side takes to engage.
out.native_mesh_prepared =
(latch.mesh_published || latch.waiting_for_driving) && !g_state.native_wheel_fallback;
return true;
}
// After the frame's draws: drop the wheel copies, and let the placeholder VR
// wheel stand in (when enabled) while no draw takes them (the race's opening
// pan, for one). Copies keep being published, so the vehicle's own wheel
// returns as soon as they match.
void FinishNativeWheelFrameLocked() noexcept {
if (!g_state.wheel_arrays_posted) {
return;
}
DropNativeWheelLocked();
if (!g_state.cockpit.mesh_published) {
return;
}
const bool log = g_state.native_wheel_switch_logs < kNativeWheelSwitchLogs;
// Lags a frame or two with the GX thread on; the threshold allows for it.
if (GxNativeWheel::LastDrawCount() > 0) {
if (g_state.native_wheel_fallback && log) {
++g_state.native_wheel_switch_logs;
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] native steering wheel: draws take the animated copy of vehicle 0x"
<< std::hex << g_state.cockpit.body << std::dec
<< " again; back to the vehicle's own wheel" << std::endl;
}
g_state.native_wheel_fallback = false;
g_state.native_wheel_unmatched = 0;
return;
}
if (!g_state.native_wheel_fallback && ++g_state.native_wheel_unmatched >= kNativeWheelUnmatchedFrames) {
g_state.native_wheel_fallback = true;
if (log) {
++g_state.native_wheel_switch_logs;
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] native steering wheel: no draw took the animated copy of vehicle 0x"
<< std::hex << g_state.cockpit.body << std::dec << " in "
<< kNativeWheelUnmatchedFrames << " frames; "
<< (RuntimeConfigFile::VrPlaceholderSteeringWheel()
? "drawing the placeholder VR wheel until one does"
: "the vehicle's own is drawn as the game poses it until one does")
<< std::endl;
}
}
}
void LogCockpitLocked(uint64_t frame, const Mtx34& view_from_world) noexcept {
if (!g_state.diagnostic_logging) {
return;
}
if (g_state.logged_frame != 0 && frame - g_state.logged_frame < 60) {
return;
}
const auto& anchor = g_state.anchor;
const auto& wheel = anchor.native_wheel;
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] cockpit: units/m=" << anchor.units_per_meter << ", bike=" << anchor.bike
<< ", wheel valid=" << wheel.valid << " centre m=(" << wheel.center[0] << ", "
<< wheel.center[1] << ", " << wheel.center[2] << ") radius=" << wheel.radius
<< ", 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) {
const float delta = std::abs(latch.predicted_view[i] - view_from_world[i]);
(i % 4 == 3 ? translation : rotation) = std::max(i % 4 == 3 ? translation : rotation, delta);
}
// The wheel copy only matches draws within 0.002 (rotation) and 0.1
// (translation) of the predicted view.
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] cockpit: race camera view vs scene view: rotation "
<< rotation << ", translation " << translation << std::endl;
}
}
void LogAnchorLocked(uint64_t frame, const Mtx34& anchor, const Mtx34& view_from_world,
const KartPoseRead& kart, const Mtx34& kart_from_local) noexcept {
// One set of lines per second at 60 Hz when [diagnostics] first_person_logging
// asks for them: enough to confirm the offsets on-device. They are written
// under the first-person lock, so they stay off otherwise.
if (!g_state.diagnostic_logging) {
return;
}
if (g_state.logged_frame != 0 && frame - g_state.logged_frame < 60) {
return;
}
g_state.logged_frame = frame;
// The anchor's translation is -R*a, so negating it gives the head's offset
// from the recorded camera measured in the levelled camera's own axes.
// While driving it should stay roughly constant: a little to the side, a
// little below the chase camera, and well in front of it.
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] first-person anchor: frame=" << frame << ", camera=0x"
<< std::hex << g_state.camera_address << std::dec
<< ", local racer=" << kart.player_index
<< ", head from camera (right, up, forward)=(" << -anchor[3] << ", "
<< -anchor[7] << ", " << anchor[11] << ") units" << std::endl;
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] first-person view: rows=(" << view_from_world[0] << ", "
<< view_from_world[1] << ", " << view_from_world[2] << "; "
<< view_from_world[4] << ", " << view_from_world[5] << ", "
<< view_from_world[6] << "; " << view_from_world[8] << ", "
<< view_from_world[9] << ", " << view_from_world[10]
<< "), translation=(" << view_from_world[3] << ", "
<< view_from_world[7] << ", " << view_from_world[11] << ")"
<< std::endl;
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] first-person pose: physics=0x" << std::hex << kart.physics
<< ", pose=0x" << (kart.physics + kKartPhysicsPoseOffset) << std::dec
<< ", rows=(" << kart_from_local[0] << ", " << kart_from_local[1]
<< ", " << kart_from_local[2] << "; " << kart_from_local[4] << ", "
<< kart_from_local[5] << ", " << kart_from_local[6] << "; "
<< kart_from_local[8] << ", " << kart_from_local[9] << ", "
<< kart_from_local[10] << "), translation=(" << kart_from_local[3]
<< ", " << kart_from_local[7] << ", " << kart_from_local[11] << ")"
<< std::endl;
// Both candidate cameras measured against the kart, so one run says which
// matrix actually describes the view the frame was rendered from. A real
// chase camera sits a few hundred units behind and above the kart; a value
// near zero horizontally means the matrix is kart-centred and unusable.
const auto eye_report = [&](const char* label, const Mtx34& v) {
const float cam[3] = {
-(v[0] * v[3] + v[4] * v[7] + v[8] * v[11]),
-(v[1] * v[3] + v[5] * v[7] + v[9] * v[11]),
-(v[2] * v[3] + v[6] * v[7] + v[10] * v[11]),
};
const float dx = kart_from_local[3] - cam[0];
const float dy = kart_from_local[7] - cam[1];
const float dz = kart_from_local[11] - cam[2];
RT_LOG(RT_TAG_RUNTIME)
<< "[mkw-vr] first-person eye [" << label << "]: camera=(" << cam[0] << ", " << cam[1]
<< ", " << cam[2] << "), kart-camera=(" << dx << ", " << dy << ", " << dz
<< "), horizontal=" << std::sqrt(dx * dx + dz * dz) << std::endl;
};
eye_report("scene", view_from_world);
// The same matrix as it stood before this frame's draws. The gap between
// the two is the error the old draw-boundary timing was introducing, and
// it grows with how fast the chase camera is moving.
if (g_state.armed_view_valid) {
eye_report("scene at draw entry", g_state.armed_view);
}
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] first-person pose bits: translation=(0x"
<< std::hex << std::bit_cast<uint32_t>(kart_from_local[3]) << ", 0x"
<< std::bit_cast<uint32_t>(kart_from_local[7]) << ", 0x"
<< std::bit_cast<uint32_t>(kart_from_local[11]) << ")" << std::dec
<< std::endl;
}
} // namespace
void MkwVRFirstPersonConfigure(bool enabled, const FirstPersonHeadOffsets& offsets,
float units_per_meter, FirstPersonRotation rotation) noexcept {
std::lock_guard lock(g_mutex);
g_state.enabled = enabled;
g_state.offsets = offsets;
g_state.rotation = rotation;
if (detail::IsFiniteFloat(&units_per_meter) && units_per_meter > 0.0f) {
g_state.units_per_meter = units_per_meter;
}
if (!enabled) {
g_state.anchor = {};
g_state.hold_frames = 0;
}
}
void MkwVRFirstPersonApplyConfiguredSettings() noexcept {
const float units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter();
const FirstPersonHeadOffsets offsets{
RuntimeConfigFile::VrFirstPersonHeadRightMeters(),
RuntimeConfigFile::VrFirstPersonHeadUpMeters(),
RuntimeConfigFile::VrFirstPersonHeadForwardMeters(),
};
const std::string mode = RuntimeConfigFile::VrFirstPersonRotation();
const FirstPersonRotation rotation = mode == "full" ? FirstPersonRotation::Full
: mode == "yaw_pitch" ? FirstPersonRotation::YawPitch
: FirstPersonRotation::YawOnly;
// Flat Screen mode shows the race on the menu screen through the game's own
// camera, where a hidden driver would only be missing from the kart.
MkwVRFirstPersonConfigure(RuntimeConfigFile::VrFirstPerson(false) &&
!RuntimeConfigFile::VrFlatScreen(),
offsets, units_per_meter, rotation);
MkwVRPolicySetFirstPersonUnitsPerMeter(units_per_meter);
const bool cockpit = RuntimeConfigFile::VrFirstPersonSeat() != "custom";
const float cockpit_units = RuntimeConfigFile::VrCockpitUnitsPerMeter();
{
// Same lock the guest thread applies these under.
std::lock_guard lock(g_mutex);
g_visibility.hide_driver = RuntimeConfigFile::VrFirstPersonHideDriver();
g_visibility.hidden_model = RuntimeConfigFile::VrFirstPersonHiddenModel();
const FirstPersonSeat seat = cockpit ? FirstPersonSeat::Cockpit : FirstPersonSeat::Custom;
if (seat != g_state.seat) {
// A seat change moves the head: do not hold the other seat's anchor.
g_state.anchor = {};
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();
g_state.diagnostic_logging = RuntimeConfigFile::DiagnosticsFirstPersonLogging();
g_state.native_wheel_fallback = false;
g_state.native_wheel_unmatched = 0;
}
// The cockpit publishes its exact per-frame scale with each anchor; this
// is the starting point until the first one.
if (cockpit) {
MkwVRPolicySetFirstPersonUnitsPerMeter(cockpit_units);
}
}
void MkwVRFirstPersonReset() noexcept {
std::lock_guard lock(g_mutex);
RestoreModelVisibilityLocked();
g_visibility.logged = false;
g_visibility.logged_models = false;
g_visibility.logged_range = false;
g_state.armed = false;
g_state.armed_view_valid = false;
g_state.camera_address = 0;
g_state.player_kart = {};
g_state.held_item = {};
++g_state.race_generation;
g_state.held_item.race_generation = g_state.race_generation;
g_state.anchor = {};
g_state.hold_frames = 0;
g_state.ever_valid_this_race = false;
g_state.failure_logged = false;
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);
}
bool MkwVRReadSceneView(Mtx34& view) noexcept {
return ReadSceneViewMatrix(view);
}
void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_address) noexcept {
std::lock_guard lock(g_mutex);
g_state.held_item = {};
g_state.held_item.race_generation = g_state.race_generation;
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 = {};
g_state.hold_frames = 0;
g_state.armed = false;
g_state.cockpit = {};
DropNativeWheelLocked();
RestoreModelVisibilityLocked();
return;
}
const auto player = detail::ReadLocalPlayerKart<Memory>();
if (player.failed_step != nullptr || player.accessor != g_state.player_kart.accessor) {
// Do not carry a held anchor or hidden models across an ownership
// change, including entering spectator mode or an online roster reset.
RestoreModelVisibilityLocked();
g_state.anchor = {};
g_state.hold_frames = 0;
}
g_state.player_kart = player;
if (player.failed_step == nullptr && g_state.seat == FirstPersonSeat::Cockpit) {
static bool item_archive_attempted = false;
if (!item_archive_attempted && RuntimeConfigFile::VrCockpitItemHand() != "off") {
item_archive_attempted = true;
const auto archive = DVDReadVrAsset("/Race/Common.szs");
if (!archive.empty()) aurora_set_cockpit_item_archive(archive.data(), static_cast<uint32_t>(archive.size()));
}
g_state.held_item = detail::ReadHeldItem<Memory>(player.player_index, g_state.race_generation);
}
g_state.armed = true;
g_state.armed_frame = guest_frame_index;
g_state.armed_view_valid = ReadSceneViewMatrix(g_state.armed_view);
if (g_state.seat == FirstPersonSeat::Cockpit) {
LatchCockpitLocked(guest_frame_index);
} else {
g_state.cockpit = {};
DropNativeWheelLocked();
}
// Uses last frame's verdict, since this frame's anchor is not computed
// until the seal. One frame of lag on hiding a model is not visible, and
// it keeps the player's kart drawn whenever the anchor is not engaged.
if (g_state.anchor.valid) {
ApplyModelVisibilityLocked();
} else {
RestoreModelVisibilityLocked();
}
}
void MkwVRFirstPersonCommit() noexcept {
std::lock_guard lock(g_mutex);
// After this frame's draws, whatever the anchor makes of it.
struct FinishWheel {
~FinishWheel() {
FinishNativeWheelFrameLocked();
DropHiddenModelsLocked();
}
} finish_wheel;
if (!g_state.armed) {
return;
}
g_state.armed = false;
const uint64_t guest_frame_index = g_state.armed_frame;
Mtx34 view_from_world{};
Mtx34 kart_from_local{};
Mtx34 anchor{};
FirstPersonAnchor cockpit_anchor{};
KartPoseRead kart{};
const char* failed_step = nullptr;
// The scene's own matrix first: it is what the recorded draws carry. The
// RaceCamera getter stays as a fallback, but it describes a different
// camera, so an anchor built from it cannot reach the chase view.
if (!ReadSceneViewMatrix(view_from_world) &&
!(g_state.camera_address != 0 &&
ReadRaceCameraViewMatrix(TryGetCpuContext(), g_state.camera_address, view_from_world,
ReadRaceCameraBlend()))) {
failed_step = "scene view matrix";
} else if (kart = ReadPlayerKartPose(g_state.player_kart, kart_from_local);
kart.failed_step != nullptr) {
failed_step = kart.failed_step;
} else if (g_state.seat == FirstPersonSeat::Cockpit) {
ComputeCockpitAnchorLocked(view_from_world, kart, kart_from_local, cockpit_anchor, failed_step);
} else if (!ComputeFirstPersonAnchor(view_from_world, kart_from_local,
g_state.offsets.right * g_state.units_per_meter,
g_state.offsets.up * g_state.units_per_meter,
g_state.offsets.forward * g_state.units_per_meter,
g_state.rotation, anchor)) {
failed_step = "anchor math (degenerate camera or kart frame)";
}
if (failed_step == nullptr) {
if (g_state.seat == FirstPersonSeat::Cockpit) {
g_state.anchor = cockpit_anchor;
g_state.anchor.guest_frame_index = guest_frame_index;
anchor = cockpit_anchor.anchor_from_scene;
LogCockpitLocked(guest_frame_index, view_from_world);
} else {
g_state.anchor = {anchor, true, guest_frame_index};
}
g_state.hold_frames = kHoldFrames;
g_state.ever_valid_this_race = true;
LogAnchorLocked(guest_frame_index, anchor, view_from_world, kart, kart_from_local);
return;
}
if (g_state.hold_frames > 0) {
--g_state.hold_frames;
g_state.anchor.guest_frame_index = guest_frame_index;
return;
}
if (!g_state.ever_valid_this_race && !g_state.failure_logged) {
// Once per race, naming the exact link that broke: every address below
// is a PAL RMCP01 constant, so this is what says which one to revisit.
g_state.failure_logged = true;
RT_LOG(RT_TAG_RUNTIME)
<< "[mkw-vr] first-person camera is enabled but could not resolve the "
<< failed_step << "; staying on the game's own camera (camera=0x" << std::hex
<< g_state.camera_address << ", race data=0x" << kart.race_data
<< ", local racer=" << std::dec << kart.player_index << std::hex
<< ", manager=0x" << kart.manager << ", players=0x"
<< kart.players << ", kart=0x" << kart.proxy << ", accessor=0x" << kart.accessor
<< ", body=0x" << kart.body << ", physics=0x" << kart.physics << std::dec << ")"
<< std::endl;
}
g_state.anchor = {};
}
FirstPersonAnchor MkwVRFirstPersonGetAnchor() noexcept {
std::lock_guard lock(g_mutex);
return g_state.anchor;
}
HeldItem MkwVRFirstPersonGetHeldItem() noexcept {
std::lock_guard lock(g_mutex);
return g_state.held_item;
}
} // namespace mkw::vr