// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinQt/Config/VRConfigWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Common/CommonPaths.h" #include "Common/Config/Config.h" #include "Common/Config/Layer.h" #include "Common/FileUtil.h" #include "Common/IniFile.h" #include "Core/Config/GraphicsSettings.h" #include "Core/ConfigLoaders/GameConfigLoader.h" #include "Core/ConfigManager.h" #include "DolphinQt/Config/ConfigControls/ConfigBool.h" #include "DolphinQt/Config/ConfigControls/ConfigChoice.h" #include "DolphinQt/Config/ConfigControls/ConfigFloatSlider.h" #include "DolphinQt/Config/ConfigControls/ConfigSlider.h" #include "DolphinQt/Config/GameConfigEdit.h" #include "DolphinQt/QtUtils/QtUtils.h" #include "DolphinQt/QtUtils/WrapInScrollArea.h" #include "DolphinQt/Settings.h" #include "VideoCommon/VideoConfig.h" namespace { constexpr std::string_view VR_SECTION_NAME = "Graphics.VR"; constexpr std::string_view LEGACY_VR_SECTION_NAME = "GFX.VR"; Config::Location GetVRLocation(const std::string& key) { return {Config::System::GFX, "VR", key}; } // Common::IniFile cannot round-trip the repeated `$Name` records used by the custom VR override // sections. Rewriting a per-game VR setting through IniFile therefore strips the names and merges // the override bodies. Replace only the two VR settings sections as text and preserve every other // section byte-for-byte. std::string ReadFileWithoutVRSettingsSections(const std::string& path) { std::ifstream file(path); if (!file.is_open()) return {}; std::ostringstream out; bool skipping = false; std::string line; while (std::getline(file, line)) { std::string trimmed = line; if (!trimmed.empty() && trimmed.back() == '\r') trimmed.pop_back(); if (trimmed == "[Graphics.VR]" || trimmed == "[GFX.VR]") { skipping = true; continue; } if (skipping && !trimmed.empty() && trimmed.front() == '[') skipping = false; if (!skipping) out << line << '\n'; } return out.str(); } class VRGameConfigLayerLoader final : public Config::ConfigLayerLoader { public: VRGameConfigLayerLoader(std::string game_id, std::optional revision, bool global) : ConfigLayerLoader(global ? Config::LayerType::GlobalGame : Config::LayerType::LocalGame), m_game_id(std::move(game_id)), m_revision(revision), m_global(global) { } void Load(Config::Layer* layer) override { Common::IniFile ini; const std::string directory = m_global ? File::GetSysDirectory() + GAMESETTINGSVR_DIR DIR_SEP : File::GetUserPath(D_GAMESETTINGSVR_IDX); for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(m_game_id, m_revision)) { ini.Load(directory + filename, true); } LoadSection(layer, ini.GetSection(LEGACY_VR_SECTION_NAME)); LoadSection(layer, ini.GetSection(VR_SECTION_NAME)); // Keep old per-game files useful in the new controls. They are written back using the current // names the next time the user changes a setting. const Config::Location passthrough = GetVRLocation("Passthrough"); const Config::Location legacy_ar_mode = GetVRLocation("ARMode"); if (!layer->Exists(passthrough)) { if (const std::optional value = layer->Get(legacy_ar_mode)) layer->Set(passthrough, *value); } const Config::Location forced_vbi = GetVRLocation("ForcedVBIFrequency"); const Config::Location legacy_auto_vbi = GetVRLocation("AutoVBIFromHMD"); if (!layer->Exists(forced_vbi)) { if (const std::optional value = layer->Get(legacy_auto_vbi)) { layer->Set(forced_vbi, *value ? Config::GFX_VR_FORCED_VBI_FREQUENCY_90 : Config::GFX_VR_FORCED_VBI_FREQUENCY_OFF); } } } void Save(Config::Layer* layer) override { if (m_global) return; const std::string path = GetSavePath(); std::string base = ReadFileWithoutVRSettingsSections(path); while (!base.empty() && (base.back() == '\n' || base.back() == '\r' || base.back() == ' ')) { base.pop_back(); } if (!base.empty()) base += '\n'; std::ostringstream out; out << base; bool wrote_header = false; for (const auto& [location, value] : layer->GetLayerMap()) { if (location.system != Config::System::GFX || location.section != "VR" || !value || location.key == "ARMode" || location.key == "AutoVBIFromHMD") { continue; } if (!wrote_header) { out << '[' << VR_SECTION_NAME << "]\n"; wrote_header = true; } out << location.key << " = " << *value << '\n'; } File::CreateFullPath(path); const std::string contents = out.str(); if (contents.empty()) { File::Delete(path, File::IfAbsentBehavior::NoConsoleWarning); return; } std::ofstream file(path, std::ios::trunc); file << contents; } private: static void LoadSection(Config::Layer* layer, const Common::IniFile::Section* section) { if (!section) return; for (const auto& [key, value] : section->GetValues()) layer->Set(GetVRLocation(key), value); } std::string GetSavePath() const { const std::string directory = File::GetUserPath(D_GAMESETTINGSVR_IDX); if (m_revision) { const std::string revision_path = fmt::format("{}{}r{}.ini", directory, m_game_id, *m_revision); if (File::Exists(revision_path)) return revision_path; } return fmt::format("{}{}.ini", directory, m_game_id); } const std::string m_game_id; const std::optional m_revision; const bool m_global; }; void ResetTabPages(QTabWidget* tab) { while (tab->count() > 0) { QWidget* const page = tab->widget(0); tab->removeTab(0); delete page; } } void PopulateVRConfigTab(QTabWidget* tab, const std::string& path, const std::string& game_id, std::optional revision, bool read_only) { for (const std::string& filename : ConfigLoaders::GetGameIniFilenames(game_id, revision)) { const std::string ini_path = path + filename; if (File::Exists(ini_path)) { auto* edit = new GameConfigEdit(nullptr, QString::fromStdString(ini_path), read_only); tab->addTab(edit, QString::fromStdString(filename)); } } } bool HasTab(const QTabWidget* tab, const QString& name) { for (int i = 0; i < tab->count(); ++i) { if (tab->tabText(i) == name) return true; } return false; } } // namespace VRConfigWidget::VRConfigWidget(std::string game_id, std::optional revision, QWidget* parent) : QWidget(parent), m_game_id(std::move(game_id)), m_revision(revision), m_layer(std::make_unique( std::make_unique(m_game_id, m_revision, false))), m_global_layer(std::make_unique( std::make_unique(m_game_id, m_revision, true))) { CreateWidgets(); m_saved_layer_state = GetLayerState(); connect(&Settings::Instance(), &Settings::ConfigChanged, this, [this] { SaveSettings(); }); // Config controls update their bold state on ConfigChanged. Delay this for the same reason as // Game Config: fonts are not reliably propagated until the widget hierarchy has been created. QTimer::singleShot(100, this, [] { Config::OnConfigChanged(); }); } VRConfigWidget::~VRConfigWidget() { m_layer->Save(); if (SConfig::GetInstance().GetGameID() == m_game_id) SConfig::GetInstance().ReloadGameVRConfigOverrides(); } void VRConfigWidget::CreateWidgets() { Config::Layer* const layer = m_layer.get(); const auto mark_default = [this](auto* control) { if (!m_global_layer->Exists(control->GetLocation())) return control; QFont font = control->font(); font.setItalic(true); control->setFont(font); return control; }; const auto make_bool = [this, layer, &mark_default](const QString& text, const Config::Info& setting) { return mark_default(new ConfigBool(text, setting, layer, false, m_global_layer.get())); }; const auto add_float = [this, layer, &mark_default]( QGridLayout* layout, int row, const QString& text, float minimum, float maximum, const Config::Info& setting, float step, std::function format, ConfigFloatSlider::ScaleMode scale = ConfigFloatSlider::ScaleMode::Linear) { auto* slider = mark_default(new ConfigFloatSlider(minimum, maximum, setting, step, layer, scale, m_global_layer.get())); auto* value = new QLabel; const auto update = [slider, value, format = std::move(format)] { value->setText(format(slider->GetValue())); }; update(); QObject::connect(slider, &ConfigFloatSlider::valueChanged, slider, update); if (!text.isEmpty()) layout->addWidget(new ConfigFloatLabel(text, slider), row, 0); layout->addWidget(slider, row, 1); layout->addWidget(value, row, 2); return slider; }; const auto add_int = [this, layer, &mark_default](QGridLayout* layout, int row, const QString& text, int minimum, int maximum, const Config::Info& setting, int step, std::function format) { auto* slider = mark_default( new ConfigSlider(minimum, maximum, setting, layer, step, m_global_layer.get())); auto* value = new QLabel; const auto update = [slider, value, format = std::move(format)] { value->setText(format(slider->value())); }; update(); QObject::connect(slider, &ConfigSlider::valueChanged, slider, update); layout->addWidget(new ConfigSliderLabel(text, slider), row, 0); layout->addWidget(slider, row, 1); layout->addWidget(value, row, 2); return slider; }; auto* general_page = new QWidget; auto* general_layout = new QVBoxLayout(general_page); auto* openxr_group = new QGroupBox(tr("OpenXR")); auto* openxr_layout = new QGridLayout(openxr_group); const auto get_game_or_base = [layer, this](const auto& setting) { if (layer->Exists(setting.GetLocation())) return layer->Get(setting); if (m_global_layer->Exists(setting.GetLocation())) return m_global_layer->Get(setting); return Config::GetBase(setting); }; QString legacy_mode_name = tr("Use existing settings"); if (!get_game_or_base(Config::GFX_VR_ENABLE_OPENXR)) legacy_mode_name += tr(" (Vulkan flat display)"); else if (get_game_or_base(Config::GFX_VR_FLAT_SCREEN)) legacy_mode_name += tr(" (2D screen in VR)"); else if (get_game_or_base(Config::GFX_VR_STEREO_SCREEN)) legacy_mode_name += tr(" (stereo 3D screen)"); else legacy_mode_name += tr(" (immersive VR)"); auto* presentation_mode = mark_default(new ConfigChoiceMap( {{legacy_mode_name, OpenXRPresentationMode::Legacy}, {tr("Flat Display (Vulkan)"), OpenXRPresentationMode::Vulkan}, {tr("Immersive VR"), OpenXRPresentationMode::Immersive}, {tr("Stereo 3D Screen"), OpenXRPresentationMode::StereoScreen}}, Config::GFX_VR_PRESENTATION_MODE, layer, m_global_layer.get())); presentation_mode->setToolTip( tr("Selects standard Vulkan flat output, BaijiSteamVR immersive VR, or a stereoscopic 3D " "screen in the headset.")); connect(presentation_mode, &QComboBox::currentIndexChanged, this, [layer] { const OpenXRPresentationMode mode = layer->Get(Config::GFX_VR_PRESENTATION_MODE); if (mode == OpenXRPresentationMode::Legacy) return; layer->Set(Config::GFX_VR_ENABLE_OPENXR.GetLocation(), mode != OpenXRPresentationMode::Vulkan); layer->Set(Config::GFX_VR_FLAT_SCREEN.GetLocation(), false); layer->Set(Config::GFX_VR_STEREO_SCREEN.GetLocation(), mode == OpenXRPresentationMode::StereoScreen); Config::OnConfigChanged(); }); openxr_layout->addWidget(new QLabel(tr("Presentation Mode:")), 0, 0); openxr_layout->addWidget(presentation_mode, 0, 1, 1, 2); auto* units_per_meter = add_float( openxr_layout, 1, tr("Units per Meter:"), Config::GFX_VR_UNITS_PER_METER_MIN, Config::GFX_VR_UNITS_PER_METER_MAX, Config::GFX_VR_UNITS_PER_METER, Config::GFX_VR_UNITS_PER_METER_STEP, [](float value) { return QString::asprintf(value < 10.0f ? "%.2f" : (value < 100.0f ? "%.1f" : "%.0f"), value); }, ConfigFloatSlider::ScaleMode::Exponential); units_per_meter->setToolTip( tr("Default: 1.0. Sets game-world units per real meter. " "Higher values make the world smaller and " "increase effective stereo depth relative to game geometry; lower values make it " "larger and reduce depth. Use Stereo Separation to adjust depth without changing " "world scale.")); auto* stereo_separation = add_float( openxr_layout, 2, tr("Stereo Separation (%):"), Config::GFX_VR_STEREO_SEPARATION_MIN, Config::GFX_VR_STEREO_SEPARATION_MAX, Config::GFX_VR_STEREO_SEPARATION, Config::GFX_VR_STEREO_SEPARATION_STEP, [](float value) { return QString::asprintf("%.0f%%", value); }); stereo_separation->setToolTip( tr("Scales the virtual left/right eye offset while leaving world scale and head movement " "unchanged. 100% preserves the current OpenXR eye spacing; lower values reduce stereo " "depth and 0% approaches mono. Lower separation may make strong near-field pop-out " "easier to fuse and may ease eye strain, but comfort varies. Higher values increase " "depth and may increase strain. This affects Immersive VR; Stereo 3D Screen uses the " "Stereoscopic Depth setting instead.")); #ifndef BAIJI_STEAMVR auto* mirror_view = mark_default(new ConfigChoiceMap( {{tr("Both Eyes"), OpenXRMirrorView::BothEyes}, {tr("Left Eye"), OpenXRMirrorView::LeftEye}, {tr("Right Eye"), OpenXRMirrorView::RightEye}, {tr("None"), OpenXRMirrorView::None}}, Config::GFX_VR_MIRROR_VIEW, layer, m_global_layer.get())); openxr_layout->addWidget(new QLabel(tr("Desktop Mirror View:")), 3, 0); openxr_layout->addWidget(mirror_view, 3, 1, 1, 2); #endif auto* camera_group = new QGroupBox(tr("Camera")); auto* camera_layout = new QGridLayout(camera_group); camera_layout->addWidget( make_bool(tr("Lean Back Angle (deg)"), Config::GFX_VR_ENABLE_LEAN_BACK_ANGLE), 0, 0); add_float(camera_layout, 0, QString{}, Config::GFX_VR_LEAN_BACK_ANGLE_MIN, Config::GFX_VR_LEAN_BACK_ANGLE_MAX, Config::GFX_VR_LEAN_BACK_ANGLE, Config::GFX_VR_LEAN_BACK_ANGLE_STEP, [](float value) { return QString::asprintf("%.1f", value); }); camera_layout->addWidget( make_bool(tr("Camera Forward (m)"), Config::GFX_VR_ENABLE_CAMERA_FORWARD), 1, 0); add_float(camera_layout, 1, QString{}, Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX, Config::GFX_VR_CAMERA_FORWARD, Config::GFX_VR_CAMERA_FORWARD_STEP, [](float value) { return QString::asprintf("%.1f", value); }); camera_layout->addWidget( make_bool(tr("Camera Height (m)"), Config::GFX_VR_ENABLE_CAMERA_HEIGHT), 2, 0); add_float(camera_layout, 2, QString{}, Config::GFX_VR_CAMERA_HEIGHT_MIN, Config::GFX_VR_CAMERA_HEIGHT_MAX, Config::GFX_VR_CAMERA_HEIGHT, Config::GFX_VR_CAMERA_HEIGHT_STEP, [](float value) { return QString::asprintf("%.1f", value); }); camera_layout->addWidget(make_bool(tr("Camera Anchor"), Config::GFX_VR_ENABLE_CAMERA_ANCHOR), 3, 0); add_float(camera_layout, 3, QString{}, Config::GFX_VR_CAMERA_ANCHOR_SMOOTHING_MIN, Config::GFX_VR_CAMERA_ANCHOR_SMOOTHING_MAX, Config::GFX_VR_CAMERA_ANCHOR_SMOOTHING, Config::GFX_VR_CAMERA_ANCHOR_SMOOTHING_STEP, [](float value) { return QString::asprintf("%.2f", value); }); camera_layout->addWidget( make_bool(tr("Controller Anchor"), Config::GFX_VR_ENABLE_CONTROLLER_ANCHOR), 4, 0, 1, 3); auto* virtual_screen_group = new QGroupBox(tr("Virtual Screen")); auto* virtual_screen_layout = new QGridLayout(virtual_screen_group); virtual_screen_layout->addWidget( make_bool(tr("Virtual Screen for 2D Content"), Config::GFX_VR_VIRTUAL_SCREEN), 0, 0, 1, 3); virtual_screen_layout->addWidget( make_bool(tr("Exact Screen Depth"), Config::GFX_VR_EXACT_SCREEN_DEPTH), 1, 0, 1, 3); virtual_screen_layout->addWidget( make_bool(tr("Auto-Exclude EFB Effects"), Config::GFX_VR_AUTO_NATIVE_EFB_EFFECTS), 2, 0, 1, 3); auto* screen_distance = add_float( virtual_screen_layout, 3, tr("Screen Distance (m):"), Config::GFX_VR_SCREEN_DISTANCE_MIN, Config::GFX_VR_SCREEN_DISTANCE_MAX, Config::GFX_VR_SCREEN_DISTANCE, Config::GFX_VR_SCREEN_DISTANCE_STEP, [](float value) { return QString::asprintf("%.1f", value); }); screen_distance->setToolTip( tr("Default: 2.0 m. Distance in meters to the virtual screen used for 2D content (menus, " "FMV, and HUD). The screen is fixed in space like a TV; it stays in place when you turn " "your head.")); add_float(virtual_screen_layout, 4, tr("Screen Size (m):"), Config::GFX_VR_SCREEN_SIZE_MIN, Config::GFX_VR_SCREEN_SIZE_MAX, Config::GFX_VR_SCREEN_SIZE, Config::GFX_VR_SCREEN_SIZE_STEP, [](float value) { return QString::asprintf("%.1f", value); }); add_float(virtual_screen_layout, 5, tr("Screen Depth:"), Config::GFX_VR_HUD_THICKNESS_MIN, Config::GFX_VR_HUD_THICKNESS_MAX, Config::GFX_VR_HUD_THICKNESS, Config::GFX_VR_HUD_THICKNESS_STEP, [this](float value) { return value <= 0.001f ? tr("Off") : QString::asprintf("%.2f m", value); }); add_float(virtual_screen_layout, 6, tr("Head Locked Curvature:"), Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN, Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX, Config::GFX_VR_HEAD_LOCKED_CURVATURE, Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP, [](float value) { return QString::asprintf("%.2f", value); }); auto* rendering_group = new QGroupBox(tr("Rendering")); auto* rendering_layout = new QGridLayout(rendering_group); add_int(rendering_layout, 0, tr("Clear EFB (min width):"), Config::GFX_VR_CLEAR_EFB_MIN, Config::GFX_VR_CLEAR_EFB_MAX, Config::GFX_VR_CLEAR_EFB_COPIES, Config::GFX_VR_CLEAR_EFB_STEP, [this](int value) { return value == 0 ? tr("Off") : QString::number(value); }); add_float(rendering_layout, 1, tr("VR Gamma:"), Config::GFX_VR_GAMMA_MIN, Config::GFX_VR_GAMMA_MAX, Config::GFX_VR_GAMMA, Config::GFX_VR_GAMMA_STEP, [this](float value) { return value <= 1.01f ? tr("Off") : QString::asprintf("%.1f", value); }); add_float(rendering_layout, 2, tr("Resolution Scale:"), Config::GFX_VR_RESOLUTION_SCALE_MIN, Config::GFX_VR_RESOLUTION_SCALE_MAX, Config::GFX_VR_RESOLUTION_SCALE, Config::GFX_VR_RESOLUTION_SCALE_STEP, [](float value) { return QString::asprintf("%.2fx", value); }); auto* foveation_level = mark_default( new ConfigChoiceMap({{tr("Off"), 0}, {tr("Low"), 1}, {tr("Medium"), 2}, {tr("High"), 3}}, Config::GFX_VR_FOVEATION_LEVEL, layer, m_global_layer.get())); rendering_layout->addWidget(new QLabel(tr("Foveated Rendering:")), 3, 0); rendering_layout->addWidget(foveation_level, 3, 1); rendering_layout->addWidget(make_bool(tr("Dynamic"), Config::GFX_VR_FOVEATION_DYNAMIC), 3, 2); rendering_layout->addWidget( make_bool(tr("Foveate Game Render (EFB)"), Config::GFX_VR_EFB_FOVEATION), 4, 1, 1, 2); general_layout->addWidget(openxr_group); general_layout->addWidget(camera_group); general_layout->addWidget(virtual_screen_group); general_layout->addWidget(rendering_group); general_layout->addStretch(); auto* hacks_page = new QWidget; auto* hacks_layout = new QVBoxLayout(hacks_page); auto* hacks_group = new QGroupBox(tr("VR Hacks")); auto* hacks_group_layout = new QVBoxLayout(hacks_group); hacks_group_layout->addWidget( make_bool(tr("Use Vulkan Multiview"), Config::GFX_VR_USE_VULKAN_MULTIVIEW)); hacks_group_layout->addWidget( make_bool(tr("Don't Clear Screen"), Config::GFX_VR_DONT_CLEAR_SCREEN)); hacks_group_layout->addWidget( make_bool(tr("Disable CPU Culling in VR"), Config::GFX_VR_DISABLE_CPU_CULL)); hacks_group_layout->addWidget(make_bool(tr("Remove Cinematic Bars"), Config::GFX_VR_REMOVE_BARS)); hacks_group_layout->addWidget( make_bool(tr("Frame Size from XFB Copy"), Config::GFX_VR_FRAME_SIZE_FROM_XFB)); hacks_group_layout->addWidget( make_bool(tr("Small 3D Viewports on Screen"), Config::GFX_VR_PANES_ON_SCREEN)); hacks_group_layout->addWidget( make_bool(tr("Detect Render-to-Texture Viewports"), Config::GFX_VR_DETECT_RENDER_TARGETS)); hacks_group_layout->addWidget( make_bool(tr("Ortho Scissor Fix"), Config::GFX_VR_ORTHO_SCISSOR_FIX)); hacks_group_layout->addWidget(make_bool(tr("Detect Skybox"), Config::GFX_VR_DETECT_SKYBOX)); hacks_group_layout->addWidget( make_bool(tr("Layered Palette Conversion Path"), Config::GFX_VR_METROID_THERMAL_VISOR_FIX)); auto* framerate_group = new QGroupBox(tr("Framerate")); auto* framerate_layout = new QGridLayout(framerate_group); auto* forced_vbi = mark_default( new ConfigChoiceMap({{tr("Auto"), Config::GFX_VR_FORCED_VBI_FREQUENCY_AUTO}, {tr("Off"), Config::GFX_VR_FORCED_VBI_FREQUENCY_OFF}, {tr("72 Hz"), Config::GFX_VR_FORCED_VBI_FREQUENCY_72}, {tr("90 Hz"), Config::GFX_VR_FORCED_VBI_FREQUENCY_90}, {tr("120 Hz"), Config::GFX_VR_FORCED_VBI_FREQUENCY_120}}, Config::GFX_VR_FORCED_VBI_FREQUENCY, layer, m_global_layer.get())); framerate_layout->addWidget(new QLabel(tr("Forced VBI Frequency:")), 0, 0); framerate_layout->addWidget(forced_vbi, 0, 1); auto* requested_refresh_rate = mark_default(new ConfigChoiceMap( {{tr("Auto"), Config::GFX_VR_REQUESTED_REFRESH_RATE_AUTO}, {tr("60 Hz"), Config::GFX_VR_REQUESTED_REFRESH_RATE_60}, {tr("72 Hz"), Config::GFX_VR_REQUESTED_REFRESH_RATE_72}, {tr("90 Hz"), Config::GFX_VR_REQUESTED_REFRESH_RATE_90}, {tr("120 Hz"), Config::GFX_VR_REQUESTED_REFRESH_RATE_120}}, Config::GFX_VR_REQUESTED_REFRESH_RATE, layer, m_global_layer.get())); framerate_layout->addWidget(new QLabel(tr("Requested Refresh Rate:")), 1, 0); framerate_layout->addWidget(requested_refresh_rate, 1, 1); framerate_layout->addWidget( make_bool(tr("Eager Frame Heartbeat"), Config::GFX_VR_EAGER_HEARTBEAT), 2, 0, 1, 2); auto* shaders_group = new QGroupBox(tr("Shaders")); auto* shaders_layout = new QVBoxLayout(shaders_group); shaders_layout->addWidget( make_bool(tr("Load Custom Shaders"), Config::GFX_VR_LOAD_CUSTOM_SHADERS)); auto* passthrough_group = new QGroupBox(tr("Passthrough")); auto* passthrough_layout = new QGridLayout(passthrough_group); passthrough_layout->addWidget(make_bool(tr("Enable Passthrough"), Config::GFX_VR_PASSTHROUGH), 0, 0, 1, 3); passthrough_layout->addWidget( make_bool(tr("Reveal Unrendered Areas"), Config::GFX_VR_PASSTHROUGH_REMOVE_BLACK_BG), 1, 0, 1, 3); passthrough_layout->addWidget( make_bool(tr("Remove Black EFB Clears"), Config::GFX_VR_PASSTHROUGH_REMOVE_BLACK_CLEARS), 2, 0, 1, 3); add_float(passthrough_layout, 3, tr("Scene Opacity:"), Config::GFX_VR_PASSTHROUGH_SCENE_OPACITY_MIN, Config::GFX_VR_PASSTHROUGH_SCENE_OPACITY_MAX, Config::GFX_VR_PASSTHROUGH_SCENE_OPACITY, Config::GFX_VR_PASSTHROUGH_SCENE_OPACITY_STEP, [](float value) { return QString::asprintf("%.2f", value); }); auto* coverage_mode = mark_default(new ConfigChoiceMap( {{tr("Exact"), VRPassthroughCoverageMode::Exact}, {tr("Fast"), VRPassthroughCoverageMode::Fast}}, Config::GFX_VR_PASSTHROUGH_COVERAGE_MODE, layer, m_global_layer.get())); passthrough_layout->addWidget(new QLabel(tr("Coverage Mode:")), 4, 0); passthrough_layout->addWidget(coverage_mode, 4, 1, 1, 2); auto* tracking_group = new QGroupBox(tr("Tracking")); auto* tracking_layout = new QGridLayout(tracking_group); auto* reference_space = mark_default(new ConfigChoiceMap( {{tr("LOCAL"), OpenXRReferenceSpaceMode::Local}, {tr("STAGE + Height"), OpenXRReferenceSpaceMode::StageHeight}, {tr("STAGE"), OpenXRReferenceSpaceMode::Stage}}, Config::GFX_VR_REFERENCE_SPACE_MODE, layer, m_global_layer.get())); auto* tracking_mode = mark_default(new ConfigChoiceMap( {{tr("6DoF"), OpenXRTrackingMode::Full6DoF}, {tr("3DoF"), OpenXRTrackingMode::Rotation3DoF}, {tr("None"), OpenXRTrackingMode::None}}, Config::GFX_VR_TRACKING_MODE, layer, m_global_layer.get())); tracking_layout->addWidget(new QLabel(tr("Default VR Position:")), 0, 0); tracking_layout->addWidget(reference_space, 0, 1); tracking_layout->addWidget(new QLabel(tr("Tracking Mode:")), 1, 0); tracking_layout->addWidget(tracking_mode, 1, 1); hacks_layout->addWidget(hacks_group); hacks_layout->addWidget(framerate_group); hacks_layout->addWidget(shaders_group); hacks_layout->addWidget(passthrough_group); hacks_layout->addWidget(tracking_group); hacks_layout->addStretch(); auto* editor_page = new QWidget; auto* editor_layout = new QVBoxLayout(editor_page); auto* default_group = new QGroupBox(tr("VR Default Config (Read Only)")); auto* default_layout = new QVBoxLayout(default_group); m_default_tab = new QTabWidget; default_layout->addWidget(m_default_tab); auto* local_group = new QGroupBox(tr("VR User Config")); auto* local_layout = new QVBoxLayout(local_group); m_local_tab = new QTabWidget; local_layout->addWidget(m_local_tab); editor_layout->addWidget(default_group); editor_layout->addWidget(local_group); auto* tabs = new QTabWidget; tabs->addTab(GetWrappedWidget(general_page), tr("General")); tabs->addTab(GetWrappedWidget(hacks_page), tr("Hacks")); const int editor_index = tabs->addTab(editor_page, tr("Editor")); connect(tabs, &QTabWidget::currentChanged, this, [this, editor_index](int index) { if (index == editor_index) { m_layer->Save(); RefreshEditorTabs(); } if (m_prev_tab_index == editor_index) ReloadSettings(); m_prev_tab_index = index; }); const QString help_message = tr("Italics mark default game settings and bold marks user settings. " "Right-click a control to remove its user setting."); auto* help_label = new QLabel(tr("These VR settings override core BaijiSteamVR settings.")); auto* help_widget = QtUtils::CreateIconWarning(this, QStyle::SP_MessageBoxQuestion, help_label); help_widget->setToolTip(help_message); auto* root_layout = new QVBoxLayout; root_layout->addWidget(help_widget); root_layout->addWidget(tabs); setLayout(root_layout); RefreshEditorTabs(); } void VRConfigWidget::SaveSettings() { const std::string layer_state = GetLayerState(); if (layer_state == m_saved_layer_state) return; m_layer->Save(); m_saved_layer_state = layer_state; if (SConfig::GetInstance().GetGameID() == m_game_id) SConfig::GetInstance().ReloadGameVRConfigOverrides(); } void VRConfigWidget::ReloadSettings() { m_layer->DeleteAllKeys(); m_layer->Load(); m_saved_layer_state = GetLayerState(); Config::OnConfigChanged(); if (SConfig::GetInstance().GetGameID() == m_game_id) SConfig::GetInstance().ReloadGameVRConfigOverrides(); } void VRConfigWidget::RefreshEditorTabs() { if (!m_default_tab || !m_local_tab || m_game_id.empty()) return; ResetTabPages(m_default_tab); ResetTabPages(m_local_tab); PopulateVRConfigTab(m_default_tab, File::GetSysDirectory() + GAMESETTINGSVR_DIR DIR_SEP, m_game_id, m_revision, true); PopulateVRConfigTab(m_local_tab, File::GetUserPath(D_GAMESETTINGSVR_IDX), m_game_id, m_revision, false); const QString local_tab_name = QString::fromStdString(m_game_id + ".ini"); if (!HasTab(m_local_tab, local_tab_name)) { m_local_tab->addTab( new GameConfigEdit(nullptr, QString::fromStdString(GetLocalINIPath()), false), local_tab_name); } } std::string VRConfigWidget::GetLocalINIPath() const { return File::GetUserPath(D_GAMESETTINGSVR_IDX) + m_game_id + ".ini"; } std::string VRConfigWidget::GetLayerState() const { std::string state; for (const auto& [location, value] : m_layer->GetLayerMap()) { const auto append_part = [&state](std::string_view part) { state += std::to_string(part.size()); state += ':'; state += part; }; state += std::to_string(static_cast(location.system)); state += ':'; append_part(location.section); append_part(location.key); state += value ? '1' : '0'; if (value) append_part(*value); } return state; }