Added First-person "Hide Driver" and "Hide Driver and Kart" & Improved Default FPV Settings

This commit is contained in:
iChris4 committed 2026-09-08 15:43:52 +02:00
1 parent 2742b8d9a2
commit 6440c5bc10
7 files changed
+394 -46

No files matched your search

+23 -9
View File
@@ -30,10 +30,12 @@ hud_virtual_screen = true
stop_at_display_copy = true
skip_copy_clears = true
first_person = false
first_person_units_per_meter = 10.0
first_person_head_up_meters = 1.0
first_person_units_per_meter = 30.0
first_person_head_up_meters = 3.0
first_person_head_forward_meters = 0.0
first_person_head_right_meters = 0.0
first_person_hide_driver = true
first_person_hidden_model = 0
```
Set `enabled = true`, close the game completely, and start it again. These settings are read only
@@ -61,8 +63,8 @@ four are live and are also exposed in the F10 settings bar.
By default the headset sits where Mario Kart's own chase camera sits, and `world_units_per_meter`
of 500 presents the race as a small diorama on a table. Turning on `first_person` moves the camera
to the Player 1 driver's head instead, and switches the world scale to
`first_person_units_per_meter`, which defaults to the 10 units per metre Mario Kart Wii is
authored at, so the race reads life-size.
`first_person_units_per_meter`, whose default of 30 is what makes the race read life-size from the
seat. It is a matter of taste rather than a property of the game, so the F10 bar exposes it.
The game's own transforms are never modified. Each guest frame the runtime reads the race camera's
view matrix and the player kart's physics pose and derives one affine transform from the recorded
@@ -82,11 +84,23 @@ immersive stereo. Menus, split-screen, and the virtual-screen fallback are unaff
the desktop mirror, which keeps showing the game's ordinary third-person view. If the kart or
camera cannot be read the camera stays where the game put it rather than guessing.
Two limitations are worth knowing. Mario Kart still culls the scene from its own chase camera, so
a wide head turn in first person can reveal the edge of what the game decided to draw. And the
driver's own head is still rendered; nudge `first_person_head_forward_meters` if it intrudes. As
with the rest of the race instrumentation, the object offsets this reads are specific to the
project's supported PAL `RMCP01` translation.
Your own driver sits exactly where your eyes are, so their head would fill the view.
`first_person_hide_driver` removes it. The game applies one draw byte across every model of a kart
and to its body, so clearing it outright takes the vehicle along with the driver;
`first_person_hidden_model` names a single model to hide instead. On PAL `RMCP01` a kart carries two
models and index `0` is the driver, which is the default: the character goes and the vehicle stays.
`-1` restores the blunt behaviour and hides everything. An index the kart does not have hides
nothing, and the log reports how many it has when the mode engages. The F10 bar presents this as
two toggles, "Hide driver" and "Hide driver and kart", alongside a button that restores every
first-person default. Both settings touch your own kart
only, so the other racers are untouched, and the original values are restored when first person
stops or the race ends. This is the one place the first-person camera modifies the game rather than
only reading it.
One limitation is worth knowing: Mario Kart still culls the scene from its own chase camera, so a
wide head turn in first person can reveal the edge of what the game decided to draw. As with the
rest of the race instrumentation, the object offsets this reads are specific to the project's
supported PAL `RMCP01` translation.
## Presentation policy
+69 -16
View File
@@ -60,6 +60,8 @@ struct RuntimeUserConfig {
std::optional<float> vrFirstPersonHeadUpMeters;
std::optional<float> vrFirstPersonHeadForwardMeters;
std::optional<float> vrFirstPersonHeadRightMeters;
std::optional<bool> vrFirstPersonHideDriver;
std::optional<int32_t> vrFirstPersonHiddenModel;
std::optional<float> audioVolume;
std::optional<float> audioMusicVolume;
std::optional<float> audioSoundEffectsVolume;
@@ -134,6 +136,17 @@ inline constexpr const char* kPortableUserDataDirectoryName = "UserData";
// installation.
inline constexpr int kPortableSearchDepth = 4;
// First-person camera defaults and the range its head offsets accept, in one
// place: the config getters, the on-disk template and the F10 bar's reset all
// read them from here, so they cannot drift apart again.
inline constexpr float kVrFirstPersonUnitsPerMeterDefault = 30.0f;
inline constexpr float kVrFirstPersonHeadUpDefault = 3.0f;
inline constexpr float kVrFirstPersonHeadForwardDefault = 0.0f;
inline constexpr float kVrFirstPersonHeadRightDefault = 0.0f;
inline constexpr bool kVrFirstPersonHideDriverDefault = true;
inline constexpr int32_t kVrFirstPersonHiddenModelDefault = 0;
inline constexpr float kVrFirstPersonHeadOffsetLimit = 10.0f;
inline std::string Trim(std::string_view text) {
size_t begin = 0;
while (begin < text.size() && std::isspace(static_cast<unsigned char>(text[begin]))) {
@@ -351,11 +364,18 @@ inline void EnsureConfigFile() {
"# below replaces world_units_per_meter while it is engaged: 10 is\n"
"# life-size, where the 500 above makes the race a small diorama.\n"
"first_person = false\n"
"first_person_units_per_meter = 10.0\n"
"first_person_units_per_meter = 30.0\n"
"# Where the head sits in the kart's own frame, in metres.\n"
"first_person_head_up_meters = 1.0\n"
"first_person_head_up_meters = 3.0\n"
"first_person_head_forward_meters = 12.0\n"
"first_person_head_right_meters = 0.0\n\n"
"first_person_head_right_meters = 0.0\n"
"# In first person the driver sits where your eyes are. Hiding\n"
"# the driver removes the head that would otherwise be in the\n"
"# way; hiding the kart removes the vehicle around you too.\n"
"first_person_hide_driver = true\n"
"# 0 is the driver, which is the usual choice. -1 hides every\n"
"# model of your kart, the vehicle included.\n"
"first_person_hidden_model = 0\n\n"
"[audio]\n"
"volume = 1.0\n"
"music_volume = 1.0\n"
@@ -522,17 +542,26 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.vrFirstPersonUnitsPerMeter = *value;
}
if (auto value = FindConfigFloat(document, "vr", "first_person_head_up_meters");
value && *value >= -3.0f && *value <= 3.0f) {
value && *value >= -kVrFirstPersonHeadOffsetLimit &&
*value <= kVrFirstPersonHeadOffsetLimit) {
config.vrFirstPersonHeadUpMeters = *value;
}
if (auto value = FindConfigFloat(document, "vr", "first_person_head_forward_meters");
value && *value >= -3.0f && *value <= 3.0f) {
value && *value >= -kVrFirstPersonHeadOffsetLimit &&
*value <= kVrFirstPersonHeadOffsetLimit) {
config.vrFirstPersonHeadForwardMeters = *value;
}
if (auto value = FindConfigFloat(document, "vr", "first_person_head_right_meters");
value && *value >= -3.0f && *value <= 3.0f) {
value && *value >= -kVrFirstPersonHeadOffsetLimit &&
*value <= kVrFirstPersonHeadOffsetLimit) {
config.vrFirstPersonHeadRightMeters = *value;
}
config.vrFirstPersonHideDriver =
FindConfigValue<bool>(document, "vr", "first_person_hide_driver");
if (auto value = FindConfigInt(document, "vr", "first_person_hidden_model");
value && *value >= -1 && *value <= 31) {
config.vrFirstPersonHiddenModel = static_cast<int32_t>(*value);
}
auto readVolume = [&](std::string_view key) -> std::optional<float> {
auto value = FindConfigFloat(document, "audio", key);
@@ -789,7 +818,7 @@ inline bool SetVrFirstPersonUnitsPerMeter(float value) {
}
inline bool SetVrFirstPersonHeadUpMeters(float value) {
value = std::clamp(value, -3.0f, 3.0f);
value = std::clamp(value, -kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
Mutable().vrFirstPersonHeadUpMeters = value;
std::ostringstream formatted;
formatted << value;
@@ -797,15 +826,28 @@ inline bool SetVrFirstPersonHeadUpMeters(float value) {
}
inline bool SetVrFirstPersonHeadForwardMeters(float value) {
value = std::clamp(value, -3.0f, 3.0f);
value = std::clamp(value, -kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
Mutable().vrFirstPersonHeadForwardMeters = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("vr", "first_person_head_forward_meters", formatted.str());
}
inline bool SetVrFirstPersonHideDriver(bool value) {
Mutable().vrFirstPersonHideDriver = value;
return WriteSetting("vr", "first_person_hide_driver", value ? "true" : "false");
}
inline bool SetVrFirstPersonHiddenModel(int32_t value) {
value = std::clamp(value, -1, 31);
Mutable().vrFirstPersonHiddenModel = value;
std::ostringstream formatted;
formatted << value;
return WriteSetting("vr", "first_person_hidden_model", formatted.str());
}
inline bool SetVrFirstPersonHeadRightMeters(float value) {
value = std::clamp(value, -3.0f, 3.0f);
value = std::clamp(value, -kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
Mutable().vrFirstPersonHeadRightMeters = value;
std::ostringstream formatted;
formatted << value;
@@ -1064,20 +1106,31 @@ inline bool VrFirstPerson(bool fallback = false) {
return Get().vrFirstPerson.value_or(fallback);
}
inline float VrFirstPersonUnitsPerMeter(float fallback = 10.0f) {
inline float VrFirstPersonUnitsPerMeter(float fallback = kVrFirstPersonUnitsPerMeterDefault) {
return std::clamp(Get().vrFirstPersonUnitsPerMeter.value_or(fallback), 1.0f, 10000.0f);
}
inline float VrFirstPersonHeadUpMeters(float fallback = 1.0f) {
return std::clamp(Get().vrFirstPersonHeadUpMeters.value_or(fallback), -3.0f, 3.0f);
inline float VrFirstPersonHeadUpMeters(float fallback = kVrFirstPersonHeadUpDefault) {
return std::clamp(Get().vrFirstPersonHeadUpMeters.value_or(fallback),
-kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
}
inline float VrFirstPersonHeadForwardMeters(float fallback = 0.0f) {
return std::clamp(Get().vrFirstPersonHeadForwardMeters.value_or(fallback), -20.0f, 20.0f);
inline float VrFirstPersonHeadForwardMeters(float fallback = kVrFirstPersonHeadForwardDefault) {
return std::clamp(Get().vrFirstPersonHeadForwardMeters.value_or(fallback),
-kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
}
inline float VrFirstPersonHeadRightMeters(float fallback = 0.0f) {
return std::clamp(Get().vrFirstPersonHeadRightMeters.value_or(fallback), -3.0f, 3.0f);
inline float VrFirstPersonHeadRightMeters(float fallback = kVrFirstPersonHeadRightDefault) {
return std::clamp(Get().vrFirstPersonHeadRightMeters.value_or(fallback),
-kVrFirstPersonHeadOffsetLimit, kVrFirstPersonHeadOffsetLimit);
}
inline bool VrFirstPersonHideDriver(bool fallback = kVrFirstPersonHideDriverDefault) {
return Get().vrFirstPersonHideDriver.value_or(fallback);
}
inline int32_t VrFirstPersonHiddenModel(int32_t fallback = kVrFirstPersonHiddenModelDefault) {
return std::clamp(Get().vrFirstPersonHiddenModel.value_or(fallback), -1, 31);
}
inline std::string GraphicsApi(std::string fallback = "auto") {
+5 -1
View File
@@ -23,7 +23,7 @@ inline constexpr Mtx34 kIdentityMtx34{
// frame is the EGG convention: +x right, +y up, +z forward.
struct FirstPersonHeadOffsets {
float right = 0.0f;
float up = 1.0f;
float up = 3.0f;
float forward = 0.0f;
};
@@ -183,6 +183,10 @@ inline bool ComputeFirstPersonAnchor(const Mtx34& view_from_world, const Mtx34&
void MkwVRFirstPersonConfigure(bool enabled, const FirstPersonHeadOffsets& offsets,
float units_per_meter) noexcept;
// While the anchor is driving the view the player's own models can be removed,
// since the driver otherwise sits exactly where the eyes are. This uses the
// game's own visibility fields, and puts them back when it stops.
//
// Reads the current [vr] first-person settings and applies them here and to the
// presentation policy's world scale. The single place those settings are
// interpreted, shared by startup and the F10 settings bar.
+5 -5
View File
@@ -78,11 +78,11 @@ struct MkwVRPolicyConfig {
// immersive race HUD so 2D content keeps its place across the transition.
float hud_width_meters = 2.4f;
float hud_scale = 1.0f;
// World scale used while the first-person camera is engaged. Mario Kart
// Wii is authored at roughly this many units per metre, so it is what
// makes the race read life-size; the third-person default deliberately
// does not, presenting the race as a small diorama instead.
float first_person_units_per_meter = 10.0f;
// World scale used while the first-person camera is engaged. This is what
// makes the race read life-size from the driver's seat; the third-person
// default deliberately does not, presenting it as a small diorama instead.
// Kept in step with RuntimeConfigFile's default, which the F10 bar resets to.
float first_person_units_per_meter = 30.0f;
};
struct MkwVRSceneObservation {
+53 -9
View File
@@ -106,10 +106,12 @@ 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);
float g_vrFirstPersonUnitsPerMeter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter();
float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters();
float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMeters();
float g_vrFirstPersonHeadRight = RuntimeConfigFile::VrFirstPersonHeadRightMeters();
bool g_vrFirstPersonHideDriver = RuntimeConfigFile::VrFirstPersonHideDriver();
int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel();
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
@@ -889,11 +891,11 @@ void DrawGraphicsSettings() {
}
bool headOffsetsChanged = false;
headOffsetsChanged |=
ImGui::SliderFloat("Head height (m)", &g_vrFirstPersonHeadUp, -1.0f, 3.0f, "%.2f");
ImGui::SliderFloat("Head height (m)", &g_vrFirstPersonHeadUp, -RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, "%.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");
ImGui::SliderFloat("Head sideways (m)", &g_vrFirstPersonHeadRight, -RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, RuntimeConfigFile::kVrFirstPersonHeadOffsetLimit, "%.2f");
if (headOffsetsChanged) {
RuntimeConfigFile::SetVrFirstPersonHeadUpMeters(g_vrFirstPersonHeadUp);
RuntimeConfigFile::SetVrFirstPersonHeadForwardMeters(g_vrFirstPersonHeadForward);
@@ -901,10 +903,52 @@ void DrawGraphicsSettings() {
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::TextDisabled("Where the head sits in the kart's own frame.");
ImGui::PopTextWrapPos();
// Two presentations of one setting: which models go, or none at all.
// Ticking either replaces the other, and unticking both shows everything.
const auto applyHiding = [](bool enabled, int model) {
g_vrFirstPersonHideDriver = enabled;
if (enabled) {
g_vrFirstPersonHiddenModel = model;
RuntimeConfigFile::SetVrFirstPersonHiddenModel(model);
}
RuntimeConfigFile::SetVrFirstPersonHideDriver(enabled);
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
};
bool hideDriver = g_vrFirstPersonHideDriver && g_vrFirstPersonHiddenModel >= 0;
bool hideDriverAndKart = g_vrFirstPersonHideDriver && g_vrFirstPersonHiddenModel < 0;
if (ImGui::Checkbox("Hide driver", &hideDriver)) {
applyHiding(hideDriver, RuntimeConfigFile::kVrFirstPersonHiddenModelDefault);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Removes your character and leaves the kart around you. Their head would "
"otherwise be where your eyes are. Other racers are unaffected.");
}
if (ImGui::Checkbox("Hide driver and kart", &hideDriverAndKart)) {
applyHiding(hideDriverAndKart, -1);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Removes the vehicle as well, leaving nothing of your own kart.");
}
ImGui::Separator();
if (ImGui::Button("Reset first-person defaults")) {
g_vrFirstPersonUnitsPerMeter = RuntimeConfigFile::kVrFirstPersonUnitsPerMeterDefault;
g_vrFirstPersonHeadUp = RuntimeConfigFile::kVrFirstPersonHeadUpDefault;
g_vrFirstPersonHeadForward = RuntimeConfigFile::kVrFirstPersonHeadForwardDefault;
g_vrFirstPersonHeadRight = RuntimeConfigFile::kVrFirstPersonHeadRightDefault;
g_vrFirstPersonHideDriver = RuntimeConfigFile::kVrFirstPersonHideDriverDefault;
g_vrFirstPersonHiddenModel = RuntimeConfigFile::kVrFirstPersonHiddenModelDefault;
RuntimeConfigFile::SetVrFirstPersonUnitsPerMeter(g_vrFirstPersonUnitsPerMeter);
RuntimeConfigFile::SetVrFirstPersonHeadUpMeters(g_vrFirstPersonHeadUp);
RuntimeConfigFile::SetVrFirstPersonHeadForwardMeters(g_vrFirstPersonHeadForward);
RuntimeConfigFile::SetVrFirstPersonHeadRightMeters(g_vrFirstPersonHeadRight);
RuntimeConfigFile::SetVrFirstPersonHideDriver(g_vrFirstPersonHideDriver);
RuntimeConfigFile::SetVrFirstPersonHiddenModel(g_vrFirstPersonHiddenModel);
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
ApplyVrHudVirtualScreen();
}
}
void DrawFpsOverlay() {
+238 -5
View File
@@ -10,6 +10,8 @@
#include <mutex>
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 {
@@ -68,6 +70,31 @@ constexpr uint32_t kKartBodyPhysicsOffset = 0x90u;
// 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;
// Offline Mario Kart Wii puts the local racer first, and immersive
// presentation already requires exactly one on-screen player.
constexpr uint32_t kLocalPlayerIndex = 0;
@@ -188,10 +215,27 @@ KartPoseRead ReadPlayerKartPose(Mtx34& out) noexcept {
// ---------------------------------------------------------------------------
// 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 = 10.0f;
float units_per_meter = RuntimeConfigFile::kVrFirstPersonUnitsPerMeterDefault;
uint32_t camera_address = 0;
// Armed by the draw boundary, consumed by the frame seal.
@@ -211,6 +255,176 @@ struct FirstPersonState {
std::mutex g_mutex;
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 visibility = 0;
if (!ReadGuestPointer(kKartManagerInstanceAddress, manager) ||
!ReadGuestPointer(manager + kKartManagerPlayersOffset, players) ||
!ReadGuestPointer(players + kLocalPlayerIndex * 4u, proxy) ||
!ReadGuestPointer(proxy + kKartProxyAccessorOffset, accessor) ||
!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();
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&) {
}
}
void LogAnchorLocked(uint64_t frame, const Mtx34& anchor, const Mtx34& view_from_world,
const KartPoseRead& kart, const Mtx34& kart_from_local) noexcept {
@@ -294,18 +508,28 @@ void MkwVRFirstPersonConfigure(bool enabled, const FirstPersonHeadOffsets& offse
}
void MkwVRFirstPersonApplyConfiguredSettings() noexcept {
const float units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter(10.0f);
const float units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter();
const FirstPersonHeadOffsets offsets{
RuntimeConfigFile::VrFirstPersonHeadRightMeters(0.0f),
RuntimeConfigFile::VrFirstPersonHeadUpMeters(1.0f),
RuntimeConfigFile::VrFirstPersonHeadForwardMeters(0.0f),
RuntimeConfigFile::VrFirstPersonHeadRightMeters(),
RuntimeConfigFile::VrFirstPersonHeadUpMeters(),
RuntimeConfigFile::VrFirstPersonHeadForwardMeters(),
};
MkwVRFirstPersonConfigure(RuntimeConfigFile::VrFirstPerson(false), offsets, units_per_meter);
MkwVRPolicySetFirstPersonUnitsPerMeter(units_per_meter);
{
// 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();
}
}
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;
@@ -323,11 +547,20 @@ void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_add
g_state.anchor = {};
g_state.hold_frames = 0;
g_state.armed = false;
RestoreModelVisibilityLocked();
return;
}
g_state.armed = true;
g_state.armed_frame = guest_frame_index;
g_state.armed_view_valid = ReadSceneViewMatrix(g_state.armed_view);
// 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 {
+1 -1
View File
@@ -45,7 +45,7 @@ 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);
config.first_person_units_per_meter = RuntimeConfigFile::VrFirstPersonUnitsPerMeter();
MkwVRPolicyConfigure(config);
MkwVRInstrumentationInitialize();
MkwVRFirstPersonApplyConfiguredSettings();