mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 02:00:14 +02:00
Ask the headset for a configured refresh rate, 120 Hz on the Steam Frame
[vr] refresh_rate (Hz, 0 = the headset's own) is requested through XR_FB_display_refresh_rate each time the session starts running and whenever the setting changes, matched against the runtime's own list within half a hertz. Setting it back to 0 restores the rate the session started at. A rate that is not offered, or one the runtime declines, is logged and changes nothing. The game renders 60 frames a second, so at 120 Hz every frame shows for exactly two refreshes, where 72 or 90 Hz hold some frames longer than others. The Steam Frame build defaults to 120; every other build keeps the headset's own rate unless the player picks one. The F10/headset panel and the Android launcher's Headset section offer the choice; the interpolation tooltip points at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
This commit is contained in:
8 files changed
+215
-1
No files matched your search
@@ -63,6 +63,7 @@ struct RuntimeUserConfig {
|
||||
std::optional<std::string> vrMirrorView;
|
||||
std::optional<std::string> vrControllerMode;
|
||||
std::optional<uint32_t> vrFrameInterpolationFps;
|
||||
std::optional<uint32_t> vrRefreshRate;
|
||||
std::optional<bool> vrFirstPerson;
|
||||
std::optional<bool> vrFirstPersonToggleClick;
|
||||
std::optional<float> vrFirstPersonUnitsPerMeter;
|
||||
@@ -281,6 +282,25 @@ inline bool IsSupportedVrPerformanceLevel(std::string_view value) {
|
||||
return value == "default" || value == "power_savings" || value == "sustained_low" ||
|
||||
value == "sustained_high" || value == "boost";
|
||||
}
|
||||
// The display refresh rate asked of the OpenXR runtime (XR_FB_display_refresh_rate), in Hz, each
|
||||
// time the session starts and whenever the setting changes; 0 leaves the headset's own rate. The
|
||||
// game renders 60 frames a second, so at 120 Hz every frame shows for exactly two refreshes, where
|
||||
// 72 or 90 Hz hold some frames longer than others. The Steam Frame starts at 120; elsewhere the
|
||||
// headset's own setting stays in charge. A runtime without the extension, or one that declines the
|
||||
// rate, keeps its own.
|
||||
#if defined(MKW_HEADSET_STEAM_FRAME)
|
||||
inline constexpr uint32_t kVrRefreshRateDefault = 120;
|
||||
#define MKW_VR_REFRESH_RATE_DEFAULT_TEXT "120"
|
||||
#else
|
||||
inline constexpr uint32_t kVrRefreshRateDefault = 0;
|
||||
#define MKW_VR_REFRESH_RATE_DEFAULT_TEXT "0"
|
||||
#endif
|
||||
inline constexpr uint32_t kVrRefreshRateMin = 60;
|
||||
inline constexpr uint32_t kVrRefreshRateMax = 240;
|
||||
|
||||
inline bool IsSupportedVrRefreshRate(uint32_t value) {
|
||||
return value == 0 || (value >= kVrRefreshRateMin && value <= kVrRefreshRateMax);
|
||||
}
|
||||
// Fixed foveated rendering of the immersive eyes on the Quest, in the order of
|
||||
// aurora_set_stereo_foveation's levels: the periphery is shaded in 2x2, then
|
||||
// 4x4 pixel blocks, the higher the level the closer to the centre. Whether the
|
||||
@@ -545,6 +565,11 @@ inline void EnsureConfigFile() {
|
||||
"controller_mode = \"wii_remote\"\n"
|
||||
"# VR interpolation: 0 = Off, 1 = Auto, or 72/90/120 FPS. Live.\n"
|
||||
"frame_interpolation_fps = 0\n"
|
||||
"# Display refresh rate asked of the headset, in Hz (72, 90, 120,\n"
|
||||
"# 144, ...), or 0 to leave the headset's own setting. The game runs\n"
|
||||
"# at 60, so 120 shows every frame twice. Only runtimes that let apps\n"
|
||||
"# choose (XR_FB_display_refresh_rate) take it. Live.\n"
|
||||
"refresh_rate = " MKW_VR_REFRESH_RATE_DEFAULT_TEXT "\n"
|
||||
"render_scale = " MKW_VR_RENDER_SCALE_DEFAULT_TEXT "\n"
|
||||
"world_units_per_meter = 500.0\n"
|
||||
"hud_distance_meters = 2.0\n"
|
||||
@@ -872,6 +897,9 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
value && IsSupportedVrControllerMode(*value)) {
|
||||
config.vrControllerMode = *value;
|
||||
}
|
||||
if (auto value = FindConfigUint(document, "vr", "refresh_rate"); value && IsSupportedVrRefreshRate(*value)) {
|
||||
config.vrRefreshRate = *value;
|
||||
}
|
||||
config.vrFrameInterpolationFps = FindConfigValue<uint32_t>(document, "vr", "frame_interpolation_fps");
|
||||
if (!config.vrFrameInterpolationFps) {
|
||||
// Migrate the initial experimental checkbox to Auto.
|
||||
@@ -1241,6 +1269,14 @@ inline bool SetVrFrameInterpolationFps(uint32_t value) {
|
||||
return WriteSetting("vr", "frame_interpolation_fps", std::to_string(value));
|
||||
}
|
||||
|
||||
inline bool SetVrRefreshRate(uint32_t value) {
|
||||
if (!IsSupportedVrRefreshRate(value)) {
|
||||
return false;
|
||||
}
|
||||
Mutable().vrRefreshRate = value;
|
||||
return WriteSetting("vr", "refresh_rate", std::to_string(value));
|
||||
}
|
||||
|
||||
inline bool SetVrFirstPersonRotation(std::string value) {
|
||||
if (!IsSupportedVrFirstPersonRotation(value)) {
|
||||
return false;
|
||||
@@ -1743,6 +1779,11 @@ inline uint32_t VrFrameInterpolationFps() {
|
||||
return mkw::vr::NormalizeFrameInterpolationFps(Get().vrFrameInterpolationFps.value_or(0));
|
||||
}
|
||||
|
||||
inline uint32_t VrRefreshRate(uint32_t fallback = kVrRefreshRateDefault) {
|
||||
const uint32_t value = Get().vrRefreshRate.value_or(fallback);
|
||||
return IsSupportedVrRefreshRate(value) ? value : 0;
|
||||
}
|
||||
|
||||
inline bool DiagnosticsOpenXRLogging(bool fallback = false) {
|
||||
return Get().diagnosticsOpenXRLogging.value_or(fallback);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,21 @@ inline uint32_t NormalizeFrameInterpolationFps(uint32_t value) noexcept {
|
||||
return value == 1 || value == 72 || value == 90 || value == 120 ? value : 0;
|
||||
}
|
||||
|
||||
// The display refresh rate to ask the runtime for ([vr] refresh_rate): the one it lists within half
|
||||
// a hertz of `requested` (runtimes report 119.9 or 120.00001 for 120), or 0 when none is close.
|
||||
inline float MatchDisplayRefreshRate(const float* rates, uint32_t count, float requested) noexcept {
|
||||
float best = 0.0f;
|
||||
float best_distance = 0.5f;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const float distance = rates[i] > requested ? rates[i] - requested : requested - rates[i];
|
||||
if (rates[i] > 0.0f && distance <= best_distance) {
|
||||
best = rates[i];
|
||||
best_distance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// A render-rate ceiling on the compositor's own display-time grid. Auto (1)
|
||||
// renders every tick. Fixed targets cannot increase the physical refresh rate.
|
||||
class FrameInterpolationPacing {
|
||||
|
||||
@@ -187,6 +187,9 @@ int g_vrFrameInterpolationMode = [] {
|
||||
return static_cast<int>(std::find(kVrInterpolationFps.begin(), kVrInterpolationFps.end(), value) -
|
||||
kVrInterpolationFps.begin());
|
||||
}();
|
||||
// [vr] refresh_rate choices; a value set in Config.toml outside them is shown as it is.
|
||||
constexpr std::array<uint32_t, 5> kVrRefreshRates{0, 72, 90, 120, 144};
|
||||
uint32_t g_vrRefreshRate = RuntimeConfigFile::VrRefreshRate();
|
||||
int g_vrFirstPersonHiddenModel = RuntimeConfigFile::VrFirstPersonHiddenModel();
|
||||
bool g_openxrDiagnosticsLogging = RuntimeConfigFile::DiagnosticsOpenXRLogging(false);
|
||||
bool g_firstPersonDiagnosticsLogging = RuntimeConfigFile::DiagnosticsFirstPersonLogging(false);
|
||||
@@ -1517,6 +1520,29 @@ void DrawVrSettings() {
|
||||
"off, or without a GPU that supports it: a new level applies after a restart.");
|
||||
}
|
||||
#endif
|
||||
{
|
||||
const auto rateLabel = [](uint32_t hz) {
|
||||
return hz == 0 ? std::string("Headset's own") : std::to_string(hz) + " Hz";
|
||||
};
|
||||
if (ImGui::BeginCombo("Headset refresh rate", rateLabel(g_vrRefreshRate).c_str())) {
|
||||
for (const uint32_t hz : kVrRefreshRates) {
|
||||
if (ImGui::Selectable(rateLabel(hz).c_str(), hz == g_vrRefreshRate) && hz != g_vrRefreshRate) {
|
||||
g_vrRefreshRate = hz;
|
||||
// The XR thread follows the setting and asks the runtime for it.
|
||||
RuntimeConfigFile::SetVrRefreshRate(hz);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Asks the headset for this display rate. The game runs at 60 FPS, so 120 Hz shows every "
|
||||
"frame for exactly two refreshes, where 72 and 90 Hz hold some frames longer than others. "
|
||||
"Only runtimes that let apps choose (XR_FB_display_refresh_rate: the Quest, the Steam "
|
||||
"Frame, Virtual Desktop) take it; the Headset line below shows the rate in use. Applies "
|
||||
"immediately.");
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("VR frame interpolation (experimental)", &g_vrFrameInterpolationMode,
|
||||
kVrInterpolationLabels.data(), static_cast<int>(kVrInterpolationLabels.size()))) {
|
||||
const auto target = kVrInterpolationFps[static_cast<size_t>(g_vrFrameInterpolationMode)];
|
||||
@@ -1526,7 +1552,7 @@ void DrawVrSettings() {
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Auto matches the headset refresh rate. 72, 90 and 120 cap the scene rendering rate; "
|
||||
"set the headset's refresh rate in Virtual Desktop or your VR runtime. "
|
||||
"set the headset's refresh rate above, or in Virtual Desktop or your VR runtime. "
|
||||
"The game stays at 60 Hz. Adds one game frame of scene latency; head tracking stays current. "
|
||||
"Needs GPU headroom and may show interpolation artifacts. Applies immediately.");
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR)
|
||||
#include "vr/openxr_backend.h"
|
||||
@@ -446,6 +447,8 @@ public:
|
||||
#endif
|
||||
if (has_extension("XR_FB_display_refresh_rate")) {
|
||||
runtime_->LoadFunction("xrGetDisplayRefreshRateFB", &get_display_refresh_rate_);
|
||||
runtime_->LoadFunction("xrEnumerateDisplayRefreshRatesFB", &enumerate_refresh_rates_);
|
||||
runtime_->LoadFunction("xrRequestDisplayRefreshRateFB", &request_refresh_rate_);
|
||||
}
|
||||
if (has_extension("XR_EXT_performance_settings")) {
|
||||
runtime_->LoadFunction("xrPerfSettingsSetPerformanceLevelEXT", &set_performance_level_);
|
||||
@@ -576,6 +579,8 @@ public:
|
||||
prepared_ = false;
|
||||
convert_display_time_ = nullptr;
|
||||
get_display_refresh_rate_ = nullptr;
|
||||
enumerate_refresh_rates_ = nullptr;
|
||||
request_refresh_rate_ = nullptr;
|
||||
set_performance_level_ = nullptr;
|
||||
headset_hz_.store(0, std::memory_order_relaxed);
|
||||
rendered_fps_.store(0, std::memory_order_relaxed);
|
||||
@@ -590,6 +595,10 @@ public:
|
||||
ResetTrackingOrigin();
|
||||
applied_session_run_serial_ = 0;
|
||||
session_was_active_ = false;
|
||||
refresh_rate_session_serial_ = 0;
|
||||
requested_refresh_rate_ = 0;
|
||||
session_start_refresh_rate_ = 0.0f;
|
||||
refresh_rate_changed_ = false;
|
||||
}
|
||||
|
||||
bool IsRunning() const noexcept { return running_.load(std::memory_order_acquire); }
|
||||
@@ -797,6 +806,70 @@ private:
|
||||
<< (XR_SUCCEEDED(gpu) ? "set" : "refused") << " (" << gpu << ")" << std::endl;
|
||||
}
|
||||
|
||||
// [vr] refresh_rate (XR_FB_display_refresh_rate), asked of the runtime each time the session
|
||||
// starts running and whenever the setting changes. The game renders 60 frames a second, so a
|
||||
// display at 120 Hz shows each one for exactly two refreshes. 0 gives back the rate the session
|
||||
// started at, if this changed it. A rate the runtime does not list, or declines (SteamVR may),
|
||||
// is logged and the runtime keeps its own.
|
||||
void ApplyRefreshRate(uint32_t requested, bool session_started) {
|
||||
if (runtime_ == nullptr || !runtime_->HasSession()) {
|
||||
return;
|
||||
}
|
||||
if (session_started) {
|
||||
session_start_refresh_rate_ = 0.0f;
|
||||
refresh_rate_changed_ = false;
|
||||
if (get_display_refresh_rate_ != nullptr &&
|
||||
XR_FAILED(get_display_refresh_rate_(runtime_->Session(), &session_start_refresh_rate_))) {
|
||||
session_start_refresh_rate_ = 0.0f;
|
||||
}
|
||||
}
|
||||
float target = static_cast<float>(requested);
|
||||
if (requested == 0) {
|
||||
if (!refresh_rate_changed_ || !(session_start_refresh_rate_ > 0.0f)) {
|
||||
return;
|
||||
}
|
||||
target = session_start_refresh_rate_;
|
||||
}
|
||||
if (enumerate_refresh_rates_ == nullptr || request_refresh_rate_ == nullptr) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: display refresh rate " << target
|
||||
<< " Hz not requested: the runtime does not offer XR_FB_display_refresh_rate"
|
||||
<< std::endl;
|
||||
return;
|
||||
}
|
||||
uint32_t count = 0;
|
||||
XrResult result = enumerate_refresh_rates_(runtime_->Session(), 0, &count, nullptr);
|
||||
std::vector<float> rates;
|
||||
if (XR_SUCCEEDED(result) && count > 0) {
|
||||
rates.resize(count);
|
||||
result = enumerate_refresh_rates_(runtime_->Session(), count, &count, rates.data());
|
||||
rates.resize(XR_SUCCEEDED(result) ? std::min<size_t>(count, rates.size()) : 0);
|
||||
}
|
||||
if (XR_FAILED(result) || rates.empty()) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: display refresh rate " << target
|
||||
<< " Hz not requested: the runtime lists no rates (" << result << ")" << std::endl;
|
||||
return;
|
||||
}
|
||||
std::ostringstream available;
|
||||
for (size_t i = 0; i < rates.size(); ++i) {
|
||||
available << (i == 0 ? "" : "/") << rates[i];
|
||||
}
|
||||
const float rate = MatchDisplayRefreshRate(rates.data(), static_cast<uint32_t>(rates.size()), target);
|
||||
if (rate == 0.0f) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: display refresh rate " << target
|
||||
<< " Hz is not offered (available " << available.str()
|
||||
<< " Hz); keeping the runtime's" << std::endl;
|
||||
return;
|
||||
}
|
||||
const XrResult set = request_refresh_rate_(runtime_->Session(), rate);
|
||||
if (XR_SUCCEEDED(set)) {
|
||||
refresh_rate_changed_ = requested != 0;
|
||||
}
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR: display refresh rate " << rate << " Hz "
|
||||
<< (XR_SUCCEEDED(set) ? "requested" : "refused") << " (" << set << "; available "
|
||||
<< available.str() << " Hz, session started at " << session_start_refresh_rate_
|
||||
<< " Hz)" << std::endl;
|
||||
}
|
||||
|
||||
static bool ProvideStereoFrame(uint32_t, AuroraStereoFrame* output, void* userdata) {
|
||||
auto* self = static_cast<OpenXRIntegration*>(userdata);
|
||||
if (self == nullptr || output == nullptr) {
|
||||
@@ -894,6 +967,14 @@ private:
|
||||
WaitForStopOrDelay(std::chrono::milliseconds(5));
|
||||
continue;
|
||||
}
|
||||
// The configured refresh rate, at each session start and whenever it changes.
|
||||
if (const uint32_t refresh_rate = RuntimeConfigFile::VrRefreshRate();
|
||||
session_run_serial != refresh_rate_session_serial_ || refresh_rate != requested_refresh_rate_) {
|
||||
const bool session_started = session_run_serial != refresh_rate_session_serial_;
|
||||
refresh_rate_session_serial_ = session_run_serial;
|
||||
requested_refresh_rate_ = refresh_rate;
|
||||
ApplyRefreshRate(refresh_rate, session_started);
|
||||
}
|
||||
|
||||
const MkwVRPolicySnapshot policy = MkwVRPolicyGetSnapshot();
|
||||
aurora_set_stereo_motion_logging(diagnostics::Enabled());
|
||||
@@ -1874,7 +1955,15 @@ private:
|
||||
std::chrono::steady_clock::time_point timing_start_ = std::chrono::steady_clock::now();
|
||||
uint32_t timing_submissions_ = 0;
|
||||
PFN_xrGetDisplayRefreshRateFB get_display_refresh_rate_ = nullptr;
|
||||
PFN_xrEnumerateDisplayRefreshRatesFB enumerate_refresh_rates_ = nullptr;
|
||||
PFN_xrRequestDisplayRefreshRateFB request_refresh_rate_ = nullptr;
|
||||
PFN_xrPerfSettingsSetPerformanceLevelEXT set_performance_level_ = nullptr;
|
||||
// [vr] refresh_rate as last applied, the session run it was applied in, and the rate that run
|
||||
// started at (pacing thread).
|
||||
uint64_t refresh_rate_session_serial_ = 0;
|
||||
uint32_t requested_refresh_rate_ = 0;
|
||||
float session_start_refresh_rate_ = 0.0f;
|
||||
bool refresh_rate_changed_ = false;
|
||||
#if defined(_WIN32)
|
||||
using ConvertDisplayTime = XrResult (XRAPI_PTR*)(XrInstance, XrTime, LARGE_INTEGER*);
|
||||
#else
|
||||
|
||||
@@ -12,6 +12,13 @@ static void Require(bool condition) {
|
||||
}
|
||||
|
||||
int main() {
|
||||
// [vr] refresh_rate picks the runtime's own value for the rate asked.
|
||||
const float frame_rates[] = {72.0f, 90.0f, 119.98f, 144.0f};
|
||||
Require(mkw::vr::MatchDisplayRefreshRate(frame_rates, 4, 120.0f) == 119.98f);
|
||||
Require(mkw::vr::MatchDisplayRefreshRate(frame_rates, 4, 90.0f) == 90.0f);
|
||||
Require(mkw::vr::MatchDisplayRefreshRate(frame_rates, 4, 60.0f) == 0.0f);
|
||||
Require(mkw::vr::MatchDisplayRefreshRate(frame_rates, 4, 100.0f) == 0.0f);
|
||||
Require(mkw::vr::MatchDisplayRefreshRate(frame_rates, 0, 120.0f) == 0.0f);
|
||||
for (uint32_t target : {0u, 1u, 72u, 90u, 120u}) {
|
||||
std::istringstream input("[vr]\nframe_interpolation_fps = " + std::to_string(target) + "\n");
|
||||
const auto config = RuntimeConfigFile::ParseConfig(input);
|
||||
|
||||
@@ -50,6 +50,24 @@ int main() {
|
||||
Require(RuntimeConfigFile::VrFoveationLevelIndex("high") == 3);
|
||||
Require(RuntimeConfigFile::VrFoveationLevelIndex("ultra") == 0);
|
||||
|
||||
// [vr] refresh_rate: Hz asked of the runtime, 0 leaving its own. The Steam Frame
|
||||
// starts at 120, twice the game's 60.
|
||||
Require(Parse("[vr]\nrefresh_rate = 120\n").vrRefreshRate == 120u);
|
||||
Require(Parse("[vr]\nrefresh_rate = 0\n").vrRefreshRate == 0u);
|
||||
Require(Parse("[vr]\nrefresh_rate = 144\n").vrRefreshRate == 144u);
|
||||
Require(!Parse("[vr]\nrefresh_rate = 30\n").vrRefreshRate.has_value());
|
||||
Require(!Parse("[vr]\nrefresh_rate = 500\n").vrRefreshRate.has_value());
|
||||
Require(!Parse("[vr]\nrefresh_rate = -1\n").vrRefreshRate.has_value());
|
||||
Require(!Parse("[vr]\nrefresh_rate = \"120\"\n").vrRefreshRate.has_value());
|
||||
Require(!Parse("[vr]\n").vrRefreshRate.has_value());
|
||||
#if defined(MKW_HEADSET_STEAM_FRAME)
|
||||
Require(RuntimeConfigFile::kVrRefreshRateDefault == 120u);
|
||||
Require(std::string_view(MKW_VR_REFRESH_RATE_DEFAULT_TEXT) == "120");
|
||||
#else
|
||||
Require(RuntimeConfigFile::kVrRefreshRateDefault == 0u);
|
||||
Require(std::string_view(MKW_VR_REFRESH_RATE_DEFAULT_TEXT) == "0");
|
||||
#endif
|
||||
|
||||
// [vr] hand_tracking: the cockpit hands follow the headset's hand tracking.
|
||||
Require(Parse("[vr]\nhand_tracking = true\n").vrHandTracking == true);
|
||||
Require(Parse("[vr]\nhand_tracking = false\n").vrHandTracking == false);
|
||||
|
||||
Reference in new issue
Block a user