mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 10:00:27 +02:00
Added First Person Camera Option
This commit is contained in:
1 parent
7898a76a22
commit
02e5cb60a4
18 files changed
+1219
-23
No files matched your search
@@ -8,6 +8,7 @@
|
||||
#include "fiber_manager.h"
|
||||
#include "platform/host_platform.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
#include "vr/openxr_integration.h"
|
||||
|
||||
@@ -516,6 +517,27 @@ void PaceToRetraceBoundary(Clock::time_point deadline) {
|
||||
VI_HLE_ProcessRetracesDeferred(1);
|
||||
}
|
||||
|
||||
// Hands Aurora the first-person camera relocation observed while this frame's
|
||||
// GX commands were produced. It has to be published here rather than by the XR
|
||||
// pacing thread: the anchor only makes sense against the recorded camera of
|
||||
// this exact frame, and the pacing thread does not know which frame its packet
|
||||
// will be paired with.
|
||||
void PublishVrSceneAnchor() {
|
||||
static bool s_engaged = false;
|
||||
const mkw::vr::FirstPersonAnchor anchor = mkw::vr::MkwVRFirstPersonGetAnchor();
|
||||
aurora_set_stereo_scene_anchor(anchor.valid ? anchor.anchor_from_scene.data() : nullptr);
|
||||
|
||||
const bool engaged = anchor.valid;
|
||||
if (engaged == s_engaged) {
|
||||
return;
|
||||
}
|
||||
s_engaged = engaged;
|
||||
// The world scale changes with the camera, and the virtual screen's metres
|
||||
// are converted at that scale, so the two have to move together.
|
||||
mkw::vr::MkwVRPolicySetFirstPersonEngaged(engaged);
|
||||
settings_overlay::RefreshVrHudVirtualScreen();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Single owner of the Aurora frame presentation sequence: seals the active frame, optionally paces the
|
||||
@@ -584,6 +606,7 @@ void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) {
|
||||
}
|
||||
|
||||
mkw::vr::OpenXRServiceProducerFrameBoundary();
|
||||
PublishVrSceneAnchor();
|
||||
// Latch the current policy safety state into this exact Aurora job. The
|
||||
// asynchronous worker may ask for an XR packet after the guest has already
|
||||
// begun the next frame, so immersive replay is accepted only when both
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -103,6 +105,11 @@ bool g_vrEnabled = RuntimeConfigFile::VrEnabled(false);
|
||||
bool g_vrStopAtDisplayCopy = RuntimeConfigFile::VrStopAtDisplayCopy(true);
|
||||
bool g_vrSkipCopyClears = RuntimeConfigFile::VrSkipCopyClears(true);
|
||||
bool g_vrHudVirtualScreen = RuntimeConfigFile::VrHudVirtualScreen(true);
|
||||
bool g_vrFirstPerson = RuntimeConfigFile::VrFirstPerson(false);
|
||||
float g_vrFirstPersonUnitsPerMeter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter(10.0f);
|
||||
float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters(1.0f);
|
||||
float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMeters(0.0f);
|
||||
float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters(0.0f);
|
||||
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
|
||||
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
|
||||
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
|
||||
@@ -708,9 +715,11 @@ void DrawAudioSettings() {
|
||||
|
||||
// The virtual screen's placement comes from the launch-time [vr] geometry, the
|
||||
// same metres the menu quad is built from, converted into the world units the
|
||||
// eye replay works in.
|
||||
// eye replay works in. Those units follow the camera: the first-person view
|
||||
// renders at its own scale, and the screen has to be sized at the same one or
|
||||
// it would not stay 2 m across in front of the player.
|
||||
void ApplyVrHudVirtualScreen() {
|
||||
const float unitsPerMeter = RuntimeConfigFile::VrWorldUnitsPerMeter(500.0f);
|
||||
const float unitsPerMeter = mkw::vr::MkwVRPolicyGetSnapshot().EffectiveUnitsPerMeter();
|
||||
aurora_set_stereo_hud_screen(g_vrHudVirtualScreen,
|
||||
RuntimeConfigFile::VrHudWidthMeters(2.4f) * unitsPerMeter,
|
||||
RuntimeConfigFile::VrHudDistanceMeters(2.0f) * unitsPerMeter);
|
||||
@@ -851,6 +860,51 @@ void DrawGraphicsSettings() {
|
||||
"the whole view. Its size and distance are the [vr] hud_width_meters and "
|
||||
"hud_distance_meters read at launch.");
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::Text("VR camera");
|
||||
if (ImGui::Checkbox("First-person camera", &g_vrFirstPerson)) {
|
||||
RuntimeConfigFile::SetVrFirstPerson(g_vrFirstPerson);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Moves the camera to the Player 1 driver's head and keeps the horizon level, "
|
||||
"instead of riding behind the kart. Applies during a single-screen race; menus "
|
||||
"and split-screen are unaffected. The world scale below replaces "
|
||||
"world_units_per_meter while it is engaged.");
|
||||
}
|
||||
// These are the tuning loop for the anchor: the right head height is a
|
||||
// per-taste value that can only really be judged from inside the headset.
|
||||
if (ImGui::SliderFloat("World units per metre (first person)", &g_vrFirstPersonUnitsPerMeter,
|
||||
1.0f, 200.0f, "%.1f")) {
|
||||
RuntimeConfigFile::SetVrFirstPersonUnitsPerMeter(g_vrFirstPersonUnitsPerMeter);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
// The virtual screen's metres are converted at this same scale.
|
||||
ApplyVrHudVirtualScreen();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Mario Kart Wii is authored at about 10 units per metre, which is what makes the "
|
||||
"race read life-size. Raising this shrinks the world around you.");
|
||||
}
|
||||
bool headOffsetsChanged = false;
|
||||
headOffsetsChanged |=
|
||||
ImGui::SliderFloat("Head height (m)", &g_vrFirstPersonHeadUp, -1.0f, 3.0f, "%.2f");
|
||||
headOffsetsChanged |=
|
||||
ImGui::SliderFloat("Head forward (m)", &g_vrFirstPersonHeadForward, -20.0f, 20.0f, "%.2f");
|
||||
headOffsetsChanged |=
|
||||
ImGui::SliderFloat("Head sideways (m)", &g_vrFirstPersonHeadRight, -3.0f, 3.0f, "%.2f");
|
||||
if (headOffsetsChanged) {
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadUpMeters(g_vrFirstPersonHeadUp);
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadForwardMeters(g_vrFirstPersonHeadForward);
|
||||
RuntimeConfigFile::SetVrFirstPersonHeadRightMeters(g_vrFirstPersonHeadRight);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
}
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
|
||||
ImGui::TextDisabled(
|
||||
"Where the head sits in the kart's own frame. Nudge it forward if the driver's own "
|
||||
"head intrudes on the view.");
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
|
||||
void DrawFpsOverlay() {
|
||||
@@ -1072,11 +1126,14 @@ void InitializeRuntimeSettings() noexcept {
|
||||
aurora_set_stereo_skip_copy_clears(g_vrSkipCopyClears);
|
||||
ApplyVrHudVirtualScreen();
|
||||
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
|
||||
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
|
||||
g_strapInputAccepted.store(false, std::memory_order_relaxed);
|
||||
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
|
||||
PADBlockInput(false);
|
||||
}
|
||||
|
||||
void RefreshVrHudVirtualScreen() noexcept { ApplyVrHudVirtualScreen(); }
|
||||
|
||||
void HandleEvents(const AuroraEvent* events) noexcept {
|
||||
if (!events) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
|
||||
#include "memory.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
extern "C" void func_805A6C58(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;
|
||||
|
||||
// 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;
|
||||
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;
|
||||
|
||||
// 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.
|
||||
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);
|
||||
}
|
||||
|
||||
bool ReadRaceCameraViewMatrix(const CpuContext* context, uint32_t camera_address,
|
||||
Mtx34& out) 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;
|
||||
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 {
|
||||
const char* failed_step = nullptr;
|
||||
uint32_t manager = 0;
|
||||
uint32_t players = 0;
|
||||
uint32_t proxy = 0;
|
||||
uint32_t accessor = 0;
|
||||
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)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct FirstPersonState {
|
||||
bool enabled = false;
|
||||
FirstPersonHeadOffsets offsets{};
|
||||
float units_per_meter = 10.0f;
|
||||
|
||||
uint32_t camera_address = 0;
|
||||
|
||||
FirstPersonAnchor anchor{};
|
||||
int hold_frames = 0;
|
||||
bool ever_valid_this_race = false;
|
||||
bool failure_logged = false;
|
||||
uint64_t logged_frame = 0;
|
||||
};
|
||||
|
||||
std::mutex g_mutex;
|
||||
FirstPersonState g_state;
|
||||
|
||||
void LogAnchorLocked(uint64_t frame, const Mtx34& anchor, const Mtx34& view_from_world,
|
||||
const KartPoseRead& kart, const Mtx34& kart_from_local) noexcept {
|
||||
// One line per second at 60 Hz: enough to confirm the offsets on-device
|
||||
// without drowning the log during a race.
|
||||
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
|
||||
<< ", 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;
|
||||
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) noexcept {
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_state.enabled = enabled;
|
||||
g_state.offsets = offsets;
|
||||
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(10.0f);
|
||||
const FirstPersonHeadOffsets offsets{
|
||||
RuntimeConfigFile::VrFirstPersonHeadRightMeters(0.0f),
|
||||
RuntimeConfigFile::VrFirstPersonHeadUpMeters(1.0f),
|
||||
RuntimeConfigFile::VrFirstPersonHeadForwardMeters(0.0f),
|
||||
};
|
||||
MkwVRFirstPersonConfigure(RuntimeConfigFile::VrFirstPerson(false), offsets, units_per_meter);
|
||||
MkwVRPolicySetFirstPersonUnitsPerMeter(units_per_meter);
|
||||
}
|
||||
|
||||
void MkwVRFirstPersonReset() noexcept {
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_state.camera_address = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_address) noexcept {
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (!g_state.enabled) {
|
||||
g_state.anchor = {};
|
||||
g_state.hold_frames = 0;
|
||||
return;
|
||||
}
|
||||
g_state.camera_address = race_camera_address;
|
||||
|
||||
Mtx34 view_from_world{};
|
||||
Mtx34 kart_from_local{};
|
||||
Mtx34 anchor{};
|
||||
KartPoseRead kart{};
|
||||
const char* failed_step = nullptr;
|
||||
if (race_camera_address == 0) {
|
||||
failed_step = "race camera (none updated this frame)";
|
||||
} else if (!ReadRaceCameraViewMatrix(TryGetCpuContext(), race_camera_address,
|
||||
view_from_world)) {
|
||||
failed_step = "race camera view matrix";
|
||||
} else if (kart = ReadPlayerKartPose(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,
|
||||
g_state.offsets.up * g_state.units_per_meter,
|
||||
g_state.offsets.forward * g_state.units_per_meter,
|
||||
/*level_horizon=*/true, anchor)) {
|
||||
failed_step = "anchor math (degenerate camera or kart frame)";
|
||||
}
|
||||
|
||||
if (failed_step == nullptr) {
|
||||
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
|
||||
<< race_camera_address << ", 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;
|
||||
}
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "memory.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -69,6 +70,15 @@ uint32_t CameraCount(uint64_t frame) noexcept {
|
||||
return g_instrumentation.camera_count;
|
||||
}
|
||||
|
||||
// The first RaceCamera updated this frame. The game also updates cameras for
|
||||
// transitions and effects, so first-wins is what keeps the first-person anchor
|
||||
// deterministic: Mario Kart updates the racers' cameras before those.
|
||||
uint32_t FirstCamera(uint64_t frame) noexcept {
|
||||
std::lock_guard lock(g_instrumentation_mutex);
|
||||
ResetCamerasForFrameLocked(frame);
|
||||
return g_instrumentation.camera_count != 0 ? g_instrumentation.cameras[0] : 0;
|
||||
}
|
||||
|
||||
uint32_t RaceScreenCount() noexcept {
|
||||
uint32_t race_scene = 0;
|
||||
if (!Memory::TryRead32(kRaceSceneInstanceAddress, race_scene) || race_scene == 0 ||
|
||||
@@ -150,6 +160,7 @@ extern "C" void MkwVRObserveTranslatedFunctionEntry(uint32_t address,
|
||||
}
|
||||
PublishRaceScene(frame, 0);
|
||||
MkwVRPolicyInvalidateRaceCamera();
|
||||
MkwVRFirstPersonReset();
|
||||
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] entered RaceScene at frame " << frame
|
||||
<< std::endl;
|
||||
break;
|
||||
@@ -169,6 +180,10 @@ extern "C" void MkwVRObserveTranslatedFunctionEntry(uint32_t address,
|
||||
const uint32_t screen_count = RaceScreenCount();
|
||||
LogRaceEvidenceIfChanged(frame, screen_count, camera_count);
|
||||
PublishRaceScene(frame, screen_count);
|
||||
// Every kart and camera has been updated for this frame and none of
|
||||
// the frame's draws have been issued yet, so the values behind these
|
||||
// pointers are exactly the ones those draws will use.
|
||||
MkwVRFirstPersonUpdate(frame, FirstCamera(frame));
|
||||
break;
|
||||
}
|
||||
case kRaceSceneOnExit: {
|
||||
@@ -177,6 +192,7 @@ extern "C" void MkwVRObserveTranslatedFunctionEntry(uint32_t address,
|
||||
scene.guest_frame_index = frame;
|
||||
MkwVRPolicyPublishScene(scene);
|
||||
MkwVRPolicyInvalidateRaceCamera();
|
||||
MkwVRFirstPersonReset();
|
||||
RT_LOG(RT_TAG_RUNTIME) << "[mkw-vr] exited RaceScene at frame " << frame
|
||||
<< std::endl;
|
||||
break;
|
||||
|
||||
@@ -17,6 +17,7 @@ struct PolicyState {
|
||||
MkwVRCameraObservation camera{};
|
||||
uint32_t available_bindings = MkwVRBindingNone;
|
||||
bool session_active = false;
|
||||
bool first_person_engaged = false;
|
||||
uint64_t safety_generation = 1;
|
||||
};
|
||||
|
||||
@@ -56,6 +57,9 @@ MkwVRPolicyConfig SanitizeConfig(const MkwVRPolicyConfig& config) noexcept {
|
||||
if (!IsFinitePositive(&sanitized.hud_scale)) {
|
||||
sanitized.hud_scale = kDefaultConfig.hud_scale;
|
||||
}
|
||||
if (!IsFinitePositive(&sanitized.first_person_units_per_meter)) {
|
||||
sanitized.first_person_units_per_meter = kDefaultConfig.first_person_units_per_meter;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
@@ -231,6 +235,21 @@ void MkwVRPolicyInvalidateRaceCamera() noexcept {
|
||||
ApplyPolicyMutation([&] { g_policy.camera.valid = false; });
|
||||
}
|
||||
|
||||
void MkwVRPolicySetFirstPersonEngaged(bool engaged) noexcept {
|
||||
std::lock_guard<std::mutex> lock(g_policy_mutex);
|
||||
// Not routed through ApplyPolicyMutation: where the camera sits does not
|
||||
// change which content is safe to present, and advancing the safety
|
||||
// generation here would drop a frame to mono on every engage.
|
||||
g_policy.first_person_engaged = engaged;
|
||||
}
|
||||
|
||||
void MkwVRPolicySetFirstPersonUnitsPerMeter(float units_per_meter) noexcept {
|
||||
std::lock_guard<std::mutex> lock(g_policy_mutex);
|
||||
if (IsFinitePositive(&units_per_meter)) {
|
||||
g_policy.config.first_person_units_per_meter = units_per_meter;
|
||||
}
|
||||
}
|
||||
|
||||
MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept {
|
||||
std::lock_guard<std::mutex> lock(g_policy_mutex);
|
||||
MkwVRPolicySnapshot snapshot;
|
||||
@@ -240,6 +259,8 @@ MkwVRPolicySnapshot MkwVRPolicyGetSnapshot() noexcept {
|
||||
snapshot.camera = g_policy.camera;
|
||||
snapshot.available_bindings = g_policy.available_bindings;
|
||||
snapshot.session_active = g_policy.session_active;
|
||||
snapshot.first_person_engaged =
|
||||
g_policy.first_person_engaged && snapshot.presentation == VRPresentationMode::ImmersiveRace;
|
||||
snapshot.safety_generation = g_policy.safety_generation;
|
||||
snapshot.content_tag = MakeContentTag(g_policy, snapshot.presentation);
|
||||
return snapshot;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
#include "vr/mkw_vr_policy.h"
|
||||
#include "vr/mkw_vr_instrumentation.h"
|
||||
|
||||
@@ -44,8 +45,10 @@ void ConfigurePolicy(bool enabled) noexcept {
|
||||
config.world_units_per_meter = RuntimeConfigFile::VrWorldUnitsPerMeter(500.0f);
|
||||
config.hud_distance_meters = RuntimeConfigFile::VrHudDistanceMeters(2.0f);
|
||||
config.hud_width_meters = RuntimeConfigFile::VrHudWidthMeters(2.4f);
|
||||
config.first_person_units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter(10.0f);
|
||||
MkwVRPolicyConfigure(config);
|
||||
MkwVRInstrumentationInitialize();
|
||||
MkwVRFirstPersonApplyConfiguredSettings();
|
||||
}
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
@@ -487,7 +490,11 @@ private:
|
||||
|
||||
{
|
||||
std::lock_guard lock(published_mutex_);
|
||||
BuildPublishedFrame(frame, immersive, policy.config.world_units_per_meter,
|
||||
// First person renders at life-size scale, third person at the
|
||||
// configured diorama scale. Head translation and IPD are the
|
||||
// only things this multiplies, so a one-frame disagreement with
|
||||
// the camera's own switch is not observable.
|
||||
BuildPublishedFrame(frame, immersive, policy.EffectiveUnitsPerMeter(),
|
||||
policy.content_tag);
|
||||
published_.store(&published_frame_, std::memory_order_release);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user