Add first-person camera logging and adjust object culling defaults for Quest

This commit is contained in:
iChris4 committed 2026-09-29 21:17:37 +02:00
1 parent e2c4eb3c8c
commit 75f075b234
7 files changed
+106 -154

No files matched your search

+25 -2
View File
@@ -93,6 +93,7 @@ struct RuntimeUserConfig {
// F10 > Diagnostics: OpenXR pacing and presentation logging in console.log.
// Off unless set; it is a bug-report aid, not something to leave running.
std::optional<bool> diagnosticsOpenXRLogging;
std::optional<bool> diagnosticsFirstPersonLogging;
std::optional<float> audioVolume;
std::optional<float> audioMusicVolume;
std::optional<float> audioSoundEffectsVolume;
@@ -238,8 +239,17 @@ inline constexpr bool kVrNativeSteeringWheelDefault = true;
inline constexpr bool kVrHandSteeringDefault = true;
// The game hides karts and objects its own camera cannot see, which a head
// turn in VR reveals. object_culling false draws them anyway (see
// vr/mkw_vr_culling.h); it only takes effect while VR is enabled.
// vr/mkw_vr_culling.h); it only takes effect while VR is enabled. The PC
// draws them by default; the Quest keeps the game's culling, since every
// extra model costs its GPU twice. The macro is the same default for the
// config file written on first launch.
#if defined(__ANDROID__)
inline constexpr bool kVrObjectCullingDefault = true;
#define MKW_VR_OBJECT_CULLING_DEFAULT_TOML "true"
#else
inline constexpr bool kVrObjectCullingDefault = false;
#define MKW_VR_OBJECT_CULLING_DEFAULT_TOML "false"
#endif
// The cockpit hands follow the headset's hand tracking (the controllers' touch
// sensors while they are held, the cameras once they are put down, when bare
// hands also drive). Opt-in, and only offered on the Quest for now; the
@@ -585,7 +595,7 @@ inline void EnsureConfigFile() {
"# The game hides karts and objects its own camera cannot see.\n"
"# object_culling = false draws them anyway, so a head turn or a\n"
"# look over the shoulder shows them; it costs GPU time.\n"
"object_culling = true\n"
"object_culling = " MKW_VR_OBJECT_CULLING_DEFAULT_TOML "\n"
"# Hand steering (by heurazy): squeeze a grip near the wheel or\n"
"# handlebar to take hold of it with the tracked controllers, and\n"
"# turn it to steer. Releasing both grips gives steering back to the\n"
@@ -881,6 +891,8 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
readRangedFloat("wheel_tracking_grace", kVrWheelTrackingGraceMin, kVrWheelTrackingGraceMax);
config.vrWheelHaptics = FindConfigValue<bool>(document, "vr", "wheel_haptics");
config.diagnosticsOpenXRLogging = FindConfigValue<bool>(document, "diagnostics", "openxr_logging");
config.diagnosticsFirstPersonLogging =
FindConfigValue<bool>(document, "diagnostics", "first_person_logging");
auto readVolume = [&](std::string_view key) -> std::optional<float> {
auto value = FindConfigFloat(document, "audio", key);
@@ -1705,6 +1717,17 @@ inline bool SetDiagnosticsOpenXRLogging(bool value) {
return WriteSetting("diagnostics", "openxr_logging", value ? "true" : "false");
}
// The first-person camera's once-a-second anchor, view, pose and cockpit lines
// (vr/mkw_vr_first_person.cpp): off unless someone is debugging the camera.
inline bool DiagnosticsFirstPersonLogging(bool fallback = false) {
return Get().diagnosticsFirstPersonLogging.value_or(fallback);
}
inline bool SetDiagnosticsFirstPersonLogging(bool value) {
Mutable().diagnosticsFirstPersonLogging = value;
return WriteSetting("diagnostics", "first_person_logging", value ? "true" : "false");
}
inline std::string VrFirstPersonRotation(std::string fallback = kVrFirstPersonRotationDefault) {
const auto& value = Get().vrFirstPersonRotation;
return value && IsSupportedVrFirstPersonRotation(*value) ? *value : std::move(fallback);
+15 -3
View File
@@ -184,6 +184,7 @@ int g_vrFrameInterpolationMode = [] {
}();
int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel();
bool g_openxrDiagnosticsLogging = RuntimeConfigFile::DiagnosticsOpenXRLogging(false);
bool g_firstPersonDiagnosticsLogging = RuntimeConfigFile::DiagnosticsFirstPersonLogging(false);
// Config spellings and menu labels for the desktop mirror, index-matched to
// AuroraStereoMirrorView so the combo selection converts to either directly.
constexpr std::array<const char*, 5> kVrMirrorViewNames{"normal", "both", "left", "right", "none"};
@@ -1465,7 +1466,7 @@ void DrawVrSettings() {
"fully virtual; the immersive window and the Flat Screen race have the room "
"around them too. Applies immediately.");
}
// Shows the live level, which debug.wiicompiled.foveation can override.
// Shows the live level Aurora holds.
g_vrFoveation = static_cast<int>(aurora_get_stereo_foveation());
if (ImGui::Combo("Foveated rendering", &g_vrFoveation, kVrFoveationLabels.data(),
static_cast<int>(kVrFoveationLabels.size()))) {
@@ -1736,8 +1737,8 @@ void DrawVrCameraSettings() {
ImGui::SetTooltip(
"The game's own culling: karts, characters and course objects outside its chase "
"camera's view are not drawn, so a head turn or a look over the shoulder finds them "
"missing. Off draws them anyway, at some GPU cost. The draw distance is unchanged, "
"and the Flat screen race view always keeps the game's culling.");
"missing. Off draws them anyway, at some GPU cost, and is the PC's default. The draw "
"distance is unchanged, and the Flat screen race view always keeps the game's culling.");
}
ImGui::Separator();
if (ImGui::Button("Reset first-person defaults")) {
@@ -1949,6 +1950,17 @@ void DrawDiagnosticsSettings() {
ImGui::TextDisabled("OpenXR is not running, so nothing is logged until a VR session starts.");
}
ImGui::PopTextWrapPos();
if (ImGui::Checkbox("First-person camera logging", &g_firstPersonDiagnosticsLogging)) {
RuntimeConfigFile::SetDiagnosticsFirstPersonLogging(g_firstPersonDiagnosticsLogging);
mkw::vr::MkwVRFirstPersonApplyConfiguredSettings();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Writes the first-person camera's anchor, view and kart pose, and the cockpit's\n"
"wheel and view checks, to console.log once per second during a race.\n"
"For reporting a misplaced first-person view. Off by default; it costs a little\n"
"game-thread time while on. Applies immediately and is remembered.");
}
ImGui::Separator();
const bool exporting = g_logExportInProgress.load(std::memory_order_acquire);
+12 -2
View File
@@ -336,6 +336,8 @@ struct FirstPersonState {
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;
@@ -1206,6 +1208,9 @@ void FinishNativeWheelFrameLocked() noexcept {
}
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;
}
@@ -1237,8 +1242,12 @@ void LogCockpitLocked(uint64_t frame, const Mtx34& view_from_world) noexcept {
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.
// 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;
}
@@ -1356,6 +1365,7 @@ void MkwVRFirstPersonApplyConfiguredSettings() noexcept {
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;
}
+2 -127
View File
@@ -41,7 +41,6 @@
#elif defined(__ANDROID__)
#include "vr/openxr_android.h"
#include "vr/openxr_vulkan.h"
#include <sys/system_properties.h>
#include <time.h>
#include <unistd.h>
#define XR_USE_TIMESPEC
@@ -503,7 +502,7 @@ public:
// How every eye is replayed, fixed: it ends at the frame's final GXCopyDisp, so it holds the
// image the game presented; it keeps the EFB reset after a display copy, which can only be
// an earlier one's and erases what that last copy did not show; and it is drawn in one
// render pass (the Quest's debug.wiicompiled.eye_passes splits it again, for A/B timing).
// render pass.
aurora_set_stereo_stop_at_display_copy(true);
aurora_set_stereo_skip_copy_clears(false);
aurora_set_stereo_single_pass_eyes(true);
@@ -938,11 +937,8 @@ private:
// The settings panel gets a compositor layer of its own while it is
// open, and Aurora leaves it out of the eyes. A backend that could
// not make that layer has the panel drawn into the eyes instead.
const bool panel_layer = backend_->PanelLayerAvailable() && !PanelLayerForcedOff();
const bool panel_layer = backend_->PanelLayerAvailable();
aurora_set_stereo_panel_layer(panel_layer);
PollEyePassesOverride();
PollFoveationOverride();
PollWindowEyesOverride();
presentation.panel.requested = panel_layer && OpenXRSettingsPanelOpen();
// Pipeline caches are stored where their stall is least visible: once when a race
@@ -1508,11 +1504,7 @@ private:
}
bool WindowShapedEyes() const noexcept {
#if defined(__ANDROID__)
return kWindowShapedEyesSupported && !window_eyes_forced_off_;
#else
return kWindowShapedEyesSupported;
#endif
}
// The seated frame the controllers are located in for hand steering: the
@@ -1624,115 +1616,6 @@ private:
return screen;
}
// Android: `adb shell setprop debug.wiicompiled.panel_layer 0` draws the settings panel into
// the eyes again, to compare the two ways or to rule out a runtime's quad layers. Read about
// once a second.
bool PanelLayerForcedOff() noexcept {
#if defined(__ANDROID__)
if (panel_layer_poll_ == 0) {
panel_layer_poll_ = 72;
char value[PROP_VALUE_MAX] = {};
const bool off =
__system_property_get("debug.wiicompiled.panel_layer", value) > 0 && value[0] == '0';
if (off != panel_layer_forced_off_) {
panel_layer_forced_off_ = off;
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: settings panel "
<< (off ? "drawn into the eyes" : "on its own layer")
<< " (debug.wiicompiled.panel_layer)" << std::endl;
}
}
--panel_layer_poll_;
return panel_layer_forced_off_;
#else
return false;
#endif
}
// Android: `adb shell setprop debug.wiicompiled.eye_passes 0` replays each eye in one render
// pass per recorded pass again, and 1 forces the single pass, to compare the two within one
// session. An empty value restores the single pass every eye has otherwise. Read about once a
// second.
void PollEyePassesOverride() noexcept {
#if defined(__ANDROID__)
if (eye_passes_poll_ != 0) {
--eye_passes_poll_;
return;
}
eye_passes_poll_ = 72;
char value[PROP_VALUE_MAX] = {};
int override_value = -1;
if (__system_property_get("debug.wiicompiled.eye_passes", value) > 0 &&
(value[0] == '0' || value[0] == '1')) {
override_value = value[0] - '0';
}
if (override_value == eye_passes_override_) {
return;
}
eye_passes_override_ = override_value;
const bool single = override_value != 0;
aurora_set_stereo_single_pass_eyes(single);
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: eyes replayed in "
<< (single ? "one render pass" : "one render pass per recorded pass")
<< (override_value >= 0 ? " (debug.wiicompiled.eye_passes)"
: " (debug.wiicompiled.eye_passes cleared)")
<< std::endl;
#endif
}
// Android: `adb shell setprop debug.wiicompiled.foveation <0-3>` overrides the foveation level
// (off, low, medium, high) within one session, for A/B timing; an empty value hands it back to
// the settings. Levels need a session launched with foveation on (see VrFoveation). Read about
// once a second.
void PollFoveationOverride() noexcept {
#if defined(__ANDROID__)
if (foveation_poll_ != 0) {
--foveation_poll_;
return;
}
foveation_poll_ = 72;
char value[PROP_VALUE_MAX] = {};
int override_value = -1;
if (__system_property_get("debug.wiicompiled.foveation", value) > 0 && value[0] >= '0' &&
value[0] <= '3' && value[1] == '\0') {
override_value = value[0] - '0';
}
if (override_value == foveation_override_) {
return;
}
foveation_override_ = override_value;
const uint32_t level = override_value >= 0
? static_cast<uint32_t>(override_value)
: RuntimeConfigFile::VrFoveationLevelIndex(RuntimeConfigFile::VrFoveation());
aurora_set_stereo_foveation(level);
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: foveation " << RuntimeConfigFile::kVrFoveationLevels[level]
<< (override_value >= 0 ? " (debug.wiicompiled.foveation)"
: " (debug.wiicompiled.foveation cleared)")
<< (aurora_stereo_foveation_available() ? "" : "; unavailable in this session")
<< std::endl;
#endif
}
// Android: `adb shell setprop debug.wiicompiled.window_eyes 0` renders the immersive window's eyes
// whole and only masks them, as the PC does, to compare the cost within one session; an empty
// value aims them through the window again. Read about once a second.
void PollWindowEyesOverride() noexcept {
#if defined(__ANDROID__)
if (window_eyes_poll_ != 0) {
--window_eyes_poll_;
return;
}
window_eyes_poll_ = 72;
char value[PROP_VALUE_MAX] = {};
const bool off = __system_property_get("debug.wiicompiled.window_eyes", value) > 0 && value[0] == '0';
if (off != window_eyes_forced_off_) {
window_eyes_forced_off_ = off;
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: immersive window eyes "
<< (off ? "rendered whole and masked" : "aimed through the window")
<< " (debug.wiicompiled.window_eyes)" << std::endl;
}
#endif
}
// The settings panel's layer hangs exactly where its pointer hits are
// tested, the rectangle it used to cover in the eyes.
static void PlacePanelLayer(OpenXRBackendFrame& frame, const OpenXRPointerScreen& screen) noexcept {
@@ -1957,14 +1840,6 @@ private:
std::unique_ptr<OpenXRRuntime> runtime_;
std::unique_ptr<GraphicsBackend> backend_;
#if defined(__ANDROID__)
uint32_t panel_layer_poll_ = 0;
bool panel_layer_forced_off_ = false;
uint32_t eye_passes_poll_ = 0;
int eye_passes_override_ = -1;
uint32_t foveation_poll_ = 0;
int foveation_override_ = -1;
uint32_t window_eyes_poll_ = 0;
bool window_eyes_forced_off_ = false;
#endif
std::unique_ptr<OpenXRInput> input_;
std::thread pacing_thread_;
+14
View File
@@ -41,6 +41,20 @@ int main() {
Require(!Parse("[vr]\nhand_tracking = 1\n").vrHandTracking.has_value());
Require(!RuntimeConfigFile::kVrHandTrackingDefault);
// [vr] object_culling: false draws what the game camera culls. The PC
// defaults to that; the Quest keeps the game's culling.
Require(Parse("[vr]\nobject_culling = true\n").vrObjectCulling == true);
Require(Parse("[vr]\nobject_culling = false\n").vrObjectCulling == false);
Require(!Parse("[vr]\n").vrObjectCulling.has_value());
Require(!Parse("[vr]\nobject_culling = 0\n").vrObjectCulling.has_value());
#if defined(__ANDROID__)
Require(RuntimeConfigFile::kVrObjectCullingDefault);
Require(std::string_view(MKW_VR_OBJECT_CULLING_DEFAULT_TOML) == "true");
#else
Require(!RuntimeConfigFile::kVrObjectCullingDefault);
Require(std::string_view(MKW_VR_OBJECT_CULLING_DEFAULT_TOML) == "false");
#endif
// [vr] immersive_window and flat_screen: one race view in two keys, Flat
// Screen mode winning, so a file that predates the window reads as before.
using RuntimeConfigFile::VrRaceView;