Online First-person camera anchor fix

This commit is contained in:
iChris4 committed 2026-09-10 03:56:42 +02:00
1 parent 0353b76baa
commit ee34fbe419
5 files changed
+320 -46

No files matched your search

+6
View File
@@ -345,6 +345,12 @@ target_include_directories(mkw_vr_first_person_tests PRIVATE "${CMAKE_CURRENT_LI
target_compile_features(mkw_vr_first_person_tests PRIVATE cxx_std_17)
add_test(NAME mkw_vr_first_person_tests COMMAND mkw_vr_first_person_tests)
# Resolve the real local-kart pointer walk against synthetic offline/online rosters.
add_executable(mkw_vr_player_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_player_tests.cpp")
target_include_directories(mkw_vr_player_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_vr_player_tests PRIVATE cxx_std_17)
add_test(NAME mkw_vr_player_tests COMMAND mkw_vr_player_tests)
if(MKW_ENABLE_OPENXR AND MKW_PLATFORM_WINDOWS)
add_executable(mkw_openxr_replay_tests
tests/openxr_d3d12_replay_tests.cpp src/vr/openxr_d3d12.cpp)
+88
View File
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
namespace mkw::vr::detail {
struct LocalPlayerKartRead {
const char* failed_step = nullptr;
uint32_t race_data = 0;
uint32_t player_index = 0xFFu;
uint32_t manager = 0;
uint32_t players = 0;
uint32_t proxy = 0;
uint32_t accessor = 0;
};
// PAL RMCP01. Use the same screen-to-racer mapping as RaceCameraMgr::__ct
// (0x805A8468), not a kart-array slot or a search for a non-CPU racer.
// The memory provider is injectable so the actual pointer walk can be tested
// without a running guest. Production uses Memory's checked, big-endian reads.
template <typename GuestMemory>
LocalPlayerKartRead ReadLocalPlayerKart() noexcept {
// Racedata::CreateInstance (0x8052FE58): 0x809C0000 - 10456.
constexpr uint32_t kRaceDataInstanceAddress = 0x809BD728u;
// Racedata::GetPlayerIdOfLocalPlayer (0x80531F70): byte +2948 + screen.
constexpr uint32_t kHudPlayerIdsOffset = 0xB84u;
// Active race scenario, not the menu scenario. GetRacePlayerCount
// (0x8052DD30) reads +0x24; the local count is the byte at +0x26.
constexpr uint32_t kPlayerCountOffset = 0x24u;
constexpr uint32_t kLocalPlayerCountOffset = 0x26u;
// Scenario::GetPlayer (0x8052DD20): scenario +8 + index*0xF0.
// Race scenario starts at +0x20; Player::GetPlayerType (0x8052ED20)
// reads +0x10. TYPE_REAL_LOCAL is 0; TYPE_REAL_ONLINE is 4.
constexpr uint32_t kPlayerTypeOffset = 0x38u;
constexpr uint32_t kPlayerStride = 0xF0u;
constexpr uint32_t kMaxPlayers = 12;
// Kart::Manager::CreateInstance (0x8058FAA8), GetKartPlayer (0x80590100),
// and Kart::Link's shared proxy -> accessor link.
constexpr uint32_t kKartManagerInstanceAddress = 0x809C18F8u;
constexpr uint32_t kKartManagerPlayersOffset = 0x20u;
LocalPlayerKartRead read{};
const auto pointer = [](uint32_t address, uint32_t& out) {
return GuestMemory::TryRead32(address, out) && out != 0;
};
if (!pointer(kRaceDataInstanceAddress, read.race_data)) {
read.failed_step = "Racedata instance";
return read;
}
read.failed_step = "local racer mapping";
if (!GuestMemory::Contains(read.race_data, kHudPlayerIdsOffset + 1u)) {
return read;
}
try {
const uint32_t player_count = GuestMemory::Read8(read.race_data + kPlayerCountOffset);
if (player_count == 0 || player_count > kMaxPlayers ||
GuestMemory::Read8(read.race_data + kLocalPlayerCountOffset) != 1) {
return read;
}
// Immersive first person supports one local screen. Its racer may be
// anywhere in the online roster. 0xFF means no racer is assigned.
read.player_index = GuestMemory::Read8(read.race_data + kHudPlayerIdsOffset);
if (read.player_index >= player_count ||
GuestMemory::Read32(read.race_data + kPlayerTypeOffset +
read.player_index * kPlayerStride) != 0) {
return read;
}
} catch (const typename GuestMemory::AccessViolation&) {
return read;
}
if (!pointer(kKartManagerInstanceAddress, read.manager)) {
read.failed_step = "Kart::Manager instance";
} else if (!pointer(read.manager + kKartManagerPlayersOffset, read.players)) {
read.failed_step = "Kart::Manager players array";
} else if (!pointer(read.players + read.player_index * 4u, read.proxy)) {
read.failed_step = "local player kart object";
} else if (!pointer(read.proxy, read.accessor)) {
read.failed_step = "local kart accessor";
} else {
read.failed_step = nullptr;
}
return read;
}
} // namespace mkw::vr::detail
+30 -45
View File
@@ -6,6 +6,7 @@
#include "runtime_config.h"
#include "runtime_log.h"
#include "vr/mkw_vr_policy.h"
#include "vr/mkw_vr_player.h"
#include <mutex>
#include <string>
@@ -50,19 +51,9 @@ constexpr uint32_t kG3DCameraMtxStateAddress = 0x802BBAB4u;
constexpr uint32_t kG3DCameraMtxBankOffset = 0x2u;
constexpr uint32_t kG3DCameraMtxOffset = 52u;
// Kart::Manager's instance pointer. Its CreateInstance (0x8058FAA8) resolves
// the slot as 0x809C0000 + 6392 in the generated translation. Read directly
// rather than observed from Kart::Manager::Update's r3, so enabling the camera
// needs no change to the translated output: an entry observer only exists in a
// build whose translation was regenerated for it, and its absence is silent.
// This mirrors how the race scene's instance slot is reached in
// mkw_vr_instrumentation.cpp.
constexpr uint32_t kKartManagerInstanceAddress = 0x809C18F8u;
// Kart::Manager::GetKartPlayer (0x80590100): `lwz r3,0x20(r3)` then indexes.
constexpr uint32_t kKartManagerPlayersOffset = 0x20u;
// Kart::Link::GetKartPosition (0x8059020C) walks proxy -> accessor -> body ->
// physics -> dynamics; the first three links are shared by every kart accessor.
constexpr uint32_t kKartProxyAccessorOffset = 0x00u;
// 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
@@ -96,10 +87,6 @@ constexpr uint32_t kModelHolderArrayOffset = 0xD8u;
constexpr uint32_t kModelHolderCountOffset = 0xF0u;
constexpr uint32_t kMaxPlayerModels = 32;
// Offline Mario Kart Wii puts the local racer first, and immersive
// presentation already requires exactly one on-screen player.
constexpr uint32_t kLocalPlayerIndex = 0;
// 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.
@@ -184,27 +171,17 @@ bool ReadRaceCameraViewMatrix(const CpuContext* context, uint32_t camera_address
// 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 {
const char* failed_step = nullptr;
uint32_t manager = 0;
uint32_t players = 0;
uint32_t proxy = 0;
uint32_t accessor = 0;
struct KartPoseRead : detail::LocalPlayerKartRead {
uint32_t body = 0;
uint32_t physics = 0;
};
KartPoseRead ReadPlayerKartPose(Mtx34& out) noexcept {
KartPoseRead read{};
if (!ReadGuestPointer(kKartManagerInstanceAddress, read.manager)) {
read.failed_step = "Kart::Manager instance";
} else if (!ReadGuestPointer(read.manager + kKartManagerPlayersOffset, read.players)) {
read.failed_step = "Kart::Manager players array";
} else if (!ReadGuestPointer(read.players + kLocalPlayerIndex * 4u, read.proxy)) {
read.failed_step = "player kart object";
} else if (!ReadGuestPointer(read.proxy + kKartProxyAccessorOffset, read.accessor)) {
read.failed_step = "kart accessor";
} else if (!ReadGuestPointer(read.accessor + kKartAccessorBodyOffset, read.body)) {
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";
@@ -240,6 +217,7 @@ struct FirstPersonState {
FirstPersonRotation rotation = FirstPersonRotation::YawOnly;
uint32_t camera_address = 0;
detail::LocalPlayerKartRead player_kart{};
// Armed by the draw boundary, consumed by the frame seal.
bool armed = false;
uint64_t armed_frame = 0;
@@ -260,16 +238,9 @@ FirstPersonState g_state;
ModelVisibilityState g_visibility;
// Walks to the player's ModelsVisibility, or zero when the race is not up.
uint32_t ResolveModelsVisibility() noexcept {
uint32_t manager = 0;
uint32_t players = 0;
uint32_t proxy = 0;
uint32_t accessor = 0;
uint32_t ResolveModelsVisibility(uint32_t accessor) noexcept {
uint32_t visibility = 0;
if (!ReadGuestPointer(kKartManagerInstanceAddress, manager) ||
!ReadGuestPointer(manager + kKartManagerPlayersOffset, players) ||
!ReadGuestPointer(players + kLocalPlayerIndex * 4u, proxy) ||
!ReadGuestPointer(proxy + kKartProxyAccessorOffset, accessor) ||
if (accessor == 0 ||
!ReadGuestPointer(accessor + kKartAccessorModelsVisibilityOffset, visibility)) {
return 0;
}
@@ -362,7 +333,7 @@ void ApplyModelVisibilityLocked() noexcept {
RestoreModelVisibilityLocked();
return;
}
const uint32_t visibility = ResolveModelsVisibility();
const uint32_t visibility = ResolveModelsVisibility(g_state.player_kart.accessor);
if (visibility == 0 ||
!Memory::Contains(visibility + kModelsVisibilityDrawOffset, 1)) {
return;
@@ -442,6 +413,7 @@ void LogAnchorLocked(uint64_t frame, const Mtx34& anchor, const Mtx34& view_from
// 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] << ", "
@@ -541,6 +513,7 @@ void MkwVRFirstPersonReset() noexcept {
g_state.armed = false;
g_state.armed_view_valid = false;
g_state.camera_address = 0;
g_state.player_kart = {};
g_state.anchor = {};
g_state.hold_frames = 0;
g_state.ever_valid_this_race = false;
@@ -558,6 +531,15 @@ void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_add
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;
g_state.armed = true;
g_state.armed_frame = guest_frame_index;
g_state.armed_view_valid = ReadSceneViewMatrix(g_state.armed_view);
@@ -591,7 +573,8 @@ void MkwVRFirstPersonCommit() noexcept {
!(g_state.camera_address != 0 &&
ReadRaceCameraViewMatrix(TryGetCpuContext(), g_state.camera_address, view_from_world))) {
failed_step = "scene view matrix";
} else if (kart = ReadPlayerKartPose(kart_from_local); kart.failed_step != nullptr) {
} else if (kart = ReadPlayerKartPose(g_state.player_kart, kart_from_local);
kart.failed_step != nullptr) {
failed_step = kart.failed_step;
} else if (!ComputeFirstPersonAnchor(view_from_world, kart_from_local,
g_state.offsets.right * g_state.units_per_meter,
@@ -621,7 +604,9 @@ void MkwVRFirstPersonCommit() noexcept {
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 << ", manager=0x" << kart.manager << ", players=0x"
<< 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;
+191
View File
@@ -0,0 +1,191 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Exercise the production local-kart resolver against a synthetic PAL roster.
#include "vr/mkw_vr_player.h"
#include <iostream>
#include <stdexcept>
#include <unordered_map>
namespace {
int g_failures = 0;
void Check(bool condition, const char* what) {
if (!condition) {
++g_failures;
std::cerr << "FAILED: " << what << '\n';
}
}
struct GuestMemory {
using AccessViolation = std::out_of_range;
inline static std::unordered_map<uint32_t, uint8_t> bytes;
inline static uint32_t fault_address = 0;
static bool Contains(uint32_t address, uint32_t size) {
for (uint32_t i = 0; i < size; ++i) {
if (bytes.count(address + i) == 0) {
return false;
}
}
return true;
}
static uint8_t Read8(uint32_t address) {
if (address == fault_address) {
throw AccessViolation("injected read fault");
}
return bytes.at(address);
}
static uint32_t Read32(uint32_t address) {
uint32_t value = 0;
for (uint32_t i = 0; i < 4; ++i) {
value = (value << 8) | Read8(address + i);
}
return value;
}
static bool TryRead32(uint32_t address, uint32_t& out) {
try {
out = Read32(address);
return true;
} catch (const AccessViolation&) {
return false;
}
}
static void Write32(uint32_t address, uint32_t value) {
for (uint32_t i = 0; i < 4; ++i) {
bytes[address + i] = static_cast<uint8_t>(value >> (24 - 8 * i));
}
}
};
constexpr uint32_t kRaceData = 0x81000000u;
constexpr uint32_t kManager = 0x81100000u;
constexpr uint32_t kKarts = 0x81100100u;
constexpr uint32_t kProxies = 0x81200000u;
constexpr uint32_t kAccessors = 0x81300000u;
void SetLocalRacer(uint8_t player) {
GuestMemory::bytes[kRaceData + 0xB84] = player;
for (uint32_t i = 0; i < 12; ++i) {
GuestMemory::Write32(kRaceData + 0x38 + i * 0xF0, i == player ? 0 : 4);
}
}
void MakeRace(uint8_t local_player) {
GuestMemory::bytes.clear();
GuestMemory::fault_address = 0;
for (uint32_t i = 0; i < 0xB90; ++i) {
GuestMemory::bytes[kRaceData + i] = 0;
}
GuestMemory::Write32(0x809BD728u, kRaceData);
GuestMemory::Write32(0x809C18F8u, kManager);
GuestMemory::Write32(kManager + 0x20, kKarts);
GuestMemory::bytes[kRaceData + 0x24] = 12;
GuestMemory::bytes[kRaceData + 0x26] = 1;
for (uint32_t i = 0; i < 12; ++i) {
GuestMemory::Write32(kKarts + i * 4, kProxies + i * 0x100);
GuestMemory::Write32(kProxies + i * 0x100, kAccessors + i * 0x100);
}
SetLocalRacer(local_player);
}
auto Resolve() {
return mkw::vr::detail::ReadLocalPlayerKart<GuestMemory>();
}
void TestSinglePlayer() {
MakeRace(0);
for (uint32_t i = 1; i < 12; ++i) {
GuestMemory::Write32(kRaceData + 0x38 + i * 0xF0, 1); // CPU
}
const auto read = Resolve();
Check(read.failed_step == nullptr && read.player_index == 0 &&
read.accessor == kAccessors,
"offline racing still selects the local kart in slot zero");
}
void TestEveryOnlineSlot() {
for (uint8_t player = 0; player < 12; ++player) {
MakeRace(player);
const auto read = Resolve();
Check(read.failed_step == nullptr && read.player_index == player &&
read.proxy == kProxies + player * 0x100u &&
read.accessor == kAccessors + player * 0x100u,
"an all-human online roster selects only the screen's local racer");
}
}
void TestRosterRemapping() {
MakeRace(5);
Check(Resolve().accessor == kAccessors + 0x500, "initial online assignment");
SetLocalRacer(11);
Check(Resolve().accessor == kAccessors + 0xB00, "a new roster assignment is read afresh");
}
void TestNoOpponentFallback() {
for (uint32_t type : {1u, 2u, 3u, 4u, 5u}) {
MakeRace(5);
GuestMemory::Write32(kRaceData + 0x38 + 5 * 0xF0, type);
GuestMemory::Write32(kRaceData + 0x38, 0); // Another valid kart is not a fallback.
const auto read = Resolve();
Check(read.failed_step != nullptr && read.accessor == 0,
"a mapped CPU, unused, ghost, remote or absent racer cannot be an anchor");
}
MakeRace(5);
GuestMemory::Write32(kKarts + 5 * 4, 0);
Check(Resolve().failed_step != nullptr && Resolve().accessor == 0,
"a missing local kart never falls back to another valid kart");
}
void TestInvalidMapping() {
for (uint8_t player : {12, 127, 255}) {
MakeRace(5);
GuestMemory::bytes[kRaceData + 0xB84] = player;
Check(Resolve().failed_step != nullptr, "out-of-range and unassigned HUD racers fail");
}
MakeRace(5);
GuestMemory::bytes[kRaceData + 0x24] = 5;
Check(Resolve().failed_step != nullptr, "racer must be inside the active roster");
for (uint8_t count : {0, 13}) {
MakeRace(5);
GuestMemory::bytes[kRaceData + 0x24] = count;
Check(Resolve().failed_step != nullptr, "empty and corrupt rosters fail");
}
for (uint8_t count : {0, 2}) {
MakeRace(5);
GuestMemory::bytes[kRaceData + 0x26] = count;
Check(Resolve().failed_step != nullptr, "spectating and split-screen have no anchor");
}
}
void TestUnavailableGuestData() {
MakeRace(5);
GuestMemory::Write32(0x809BD728u, 0);
Check(Resolve().failed_step != nullptr, "missing race data fails");
MakeRace(5);
GuestMemory::bytes.erase(kRaceData + 0xB84);
Check(Resolve().failed_step != nullptr, "unmapped race data fails");
MakeRace(5);
GuestMemory::fault_address = kRaceData + 0x38 + 5 * 0xF0;
Check(Resolve().failed_step != nullptr, "a fault after the bounds check is contained");
MakeRace(5);
GuestMemory::bytes.erase(kProxies + 5 * 0x100);
Check(Resolve().failed_step != nullptr, "an unreadable local accessor fails");
}
} // namespace
int main() {
TestSinglePlayer();
TestEveryOnlineSlot();
TestRosterRemapping();
TestNoOpponentFallback();
TestInvalidMapping();
TestUnavailableGuestData();
return g_failures == 0 ? 0 : 1;
}