// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinQt/Settings/VRPane.h" #include #include #include #include #include #include #include "Core/Config/GraphicsSettings.h" #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/System.h" #include "DolphinQt/Config/HideObjectAddEditDialog.h" #include "DolphinQt/Config/ConfigControls/ConfigBool.h" #include "DolphinQt/Config/ConfigControls/ConfigFloatSlider.h" #include "DolphinQt/Config/ConfigControls/ConfigSlider.h" #include "DolphinQt/Debugger/ShaderHunterWidget.h" #include "DolphinQt/Settings.h" #include "VideoCommon/HideObjectEngine.h" VRPane::VRPane(QWidget* parent) : QWidget(parent) { auto* main_layout = new QVBoxLayout; auto* openxr_group = new QGroupBox(tr("OpenXR")); auto* openxr_layout = new QGridLayout; openxr_group->setLayout(openxr_layout); m_enable_openxr = new ConfigBool(tr("Enable OpenXR"), Config::GFX_VR_ENABLE_OPENXR); m_units_per_meter = new ConfigFloatSlider(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); m_units_per_meter_value = new QLabel(); m_lean_back_angle = new ConfigFloatSlider(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); m_lean_back_angle_value = new QLabel(); m_camera_forward = new ConfigFloatSlider(Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX, Config::GFX_VR_CAMERA_FORWARD, Config::GFX_VR_CAMERA_FORWARD_STEP); m_camera_forward_value = new QLabel(); openxr_layout->addWidget(m_enable_openxr, 0, 0, 1, 3); openxr_layout->addWidget(new ConfigFloatLabel(tr("Units per Meter:"), m_units_per_meter), 1, 0); openxr_layout->addWidget(m_units_per_meter, 1, 1); openxr_layout->addWidget(m_units_per_meter_value, 1, 2); openxr_layout->addWidget(new ConfigFloatLabel(tr("Lean Back Angle (deg):"), m_lean_back_angle), 2, 0); openxr_layout->addWidget(m_lean_back_angle, 2, 1); openxr_layout->addWidget(m_lean_back_angle_value, 2, 2); openxr_layout->addWidget(new ConfigFloatLabel(tr("Camera Forward (m):"), m_camera_forward), 3, 0); openxr_layout->addWidget(m_camera_forward, 3, 1); openxr_layout->addWidget(m_camera_forward_value, 3, 2); m_units_per_meter_value->setText(QString::asprintf("%.1f", m_units_per_meter->GetValue())); connect(m_units_per_meter, &ConfigFloatSlider::valueChanged, this, [this] { m_units_per_meter_value->setText(QString::asprintf("%.1f", m_units_per_meter->GetValue())); }); m_lean_back_angle_value->setText(QString::asprintf("%.1f", m_lean_back_angle->GetValue())); connect(m_lean_back_angle, &ConfigFloatSlider::valueChanged, this, [this] { m_lean_back_angle_value->setText(QString::asprintf("%.1f", m_lean_back_angle->GetValue())); }); m_camera_forward_value->setText(QString::asprintf("%.1f", m_camera_forward->GetValue())); connect(m_camera_forward, &ConfigFloatSlider::valueChanged, this, [this] { m_camera_forward_value->setText(QString::asprintf("%.1f", m_camera_forward->GetValue())); }); // Virtual screen settings m_virtual_screen = new ConfigBool(tr("Virtual Screen for 2D Content"), Config::GFX_VR_VIRTUAL_SCREEN); openxr_layout->addWidget(m_virtual_screen, 4, 0, 1, 3); m_screen_distance = new ConfigFloatSlider(Config::GFX_VR_SCREEN_DISTANCE_MIN, Config::GFX_VR_SCREEN_DISTANCE_MAX, Config::GFX_VR_SCREEN_DISTANCE, Config::GFX_VR_SCREEN_DISTANCE_STEP); m_screen_distance_value = new QLabel(); m_screen_size = new ConfigFloatSlider(Config::GFX_VR_SCREEN_SIZE_MIN, Config::GFX_VR_SCREEN_SIZE_MAX, Config::GFX_VR_SCREEN_SIZE, Config::GFX_VR_SCREEN_SIZE_STEP); m_screen_size_value = new QLabel(); m_head_locked_curvature = new ConfigFloatSlider(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); m_head_locked_curvature_value = new QLabel(); openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Distance (m):"), m_screen_distance), 5, 0); openxr_layout->addWidget(m_screen_distance, 5, 1); openxr_layout->addWidget(m_screen_distance_value, 5, 2); openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Size (m):"), m_screen_size), 6, 0); openxr_layout->addWidget(m_screen_size, 6, 1); openxr_layout->addWidget(m_screen_size_value, 6, 2); openxr_layout->addWidget( new ConfigFloatLabel(tr("Head Locked Curvature:"), m_head_locked_curvature), 7, 0); openxr_layout->addWidget(m_head_locked_curvature, 7, 1); openxr_layout->addWidget(m_head_locked_curvature_value, 7, 2); m_screen_distance_value->setText(QString::asprintf("%.1f", m_screen_distance->GetValue())); connect(m_screen_distance, &ConfigFloatSlider::valueChanged, this, [this] { m_screen_distance_value->setText(QString::asprintf("%.1f", m_screen_distance->GetValue())); }); m_screen_size_value->setText(QString::asprintf("%.1f", m_screen_size->GetValue())); connect(m_screen_size, &ConfigFloatSlider::valueChanged, this, [this] { m_screen_size_value->setText(QString::asprintf("%.1f", m_screen_size->GetValue())); }); m_head_locked_curvature_value->setText( QString::asprintf("%.2f", m_head_locked_curvature->GetValue())); connect(m_head_locked_curvature, &ConfigFloatSlider::valueChanged, this, [this] { m_head_locked_curvature_value->setText( QString::asprintf("%.2f", m_head_locked_curvature->GetValue())); }); m_dont_clear_screen = new ConfigBool(tr("Don't Clear Screen"), Config::GFX_VR_DONT_CLEAR_SCREEN); openxr_layout->addWidget(m_dont_clear_screen, 8, 0, 1, 3); m_disable_cpu_cull = new ConfigBool(tr("Disable CPU Culling in VR"), Config::GFX_VR_DISABLE_CPU_CULL); openxr_layout->addWidget(m_disable_cpu_cull, 9, 0, 1, 3); m_auto_vbi_from_hmd = new ConfigBool(tr("Force VBI Frequency to 90 Hz in VR"), Config::GFX_VR_AUTO_VBI_FROM_HMD); openxr_layout->addWidget(m_auto_vbi_from_hmd, 10, 0, 1, 3); m_clear_efb_slider = new ConfigSlider(Config::GFX_VR_CLEAR_EFB_MIN, Config::GFX_VR_CLEAR_EFB_MAX, Config::GFX_VR_CLEAR_EFB_COPIES, Config::GFX_VR_CLEAR_EFB_STEP); m_clear_efb_value = new QLabel(); openxr_layout->addWidget(new ConfigSliderLabel(tr("Clear EFB (min width):"), m_clear_efb_slider), 11, 0); openxr_layout->addWidget(m_clear_efb_slider, 11, 1); openxr_layout->addWidget(m_clear_efb_value, 11, 2); auto update_clear_efb_label = [this] { const int val = m_clear_efb_slider->value(); m_clear_efb_value->setText(val == 0 ? tr("Off") : QString::number(val)); }; update_clear_efb_label(); connect(m_clear_efb_slider, &ConfigSlider::valueChanged, this, update_clear_efb_label); // Remove Cinematic Bars m_remove_bars = new ConfigBool(tr("Remove Cinematic Bars"), Config::GFX_VR_REMOVE_BARS); openxr_layout->addWidget(m_remove_bars, 12, 0, 1, 3); // Auto Layer Spread m_auto_layer_spread = new ConfigBool(tr("Auto Layer Spread"), Config::GFX_VR_AUTO_LAYER_SPREAD); openxr_layout->addWidget(m_auto_layer_spread, 13, 0, 1, 3); m_layer_offset = new ConfigFloatSlider(Config::GFX_VR_LAYER_OFFSET_MIN, Config::GFX_VR_LAYER_OFFSET_MAX, Config::GFX_VR_LAYER_OFFSET, Config::GFX_VR_LAYER_OFFSET_STEP); m_layer_offset_value = new QLabel(); openxr_layout->addWidget(new ConfigFloatLabel(tr("Layer Offset:"), m_layer_offset), 14, 0); openxr_layout->addWidget(m_layer_offset, 14, 1); openxr_layout->addWidget(m_layer_offset_value, 14, 2); m_layer_offset_value->setText(QString::asprintf("%.4f", m_layer_offset->GetValue())); connect(m_layer_offset, &ConfigFloatSlider::valueChanged, this, [this] { m_layer_offset_value->setText(QString::asprintf("%.4f", m_layer_offset->GetValue())); }); m_element_depth = new ConfigFloatSlider(Config::GFX_VR_ELEMENT_DEPTH_MIN, Config::GFX_VR_ELEMENT_DEPTH_MAX, Config::GFX_VR_ELEMENT_DEPTH, Config::GFX_VR_ELEMENT_DEPTH_STEP); m_element_depth_value = new QLabel(); openxr_layout->addWidget(new ConfigFloatLabel(tr("Element Depth:"), m_element_depth), 15, 0); openxr_layout->addWidget(m_element_depth, 15, 1); openxr_layout->addWidget(m_element_depth_value, 15, 2); m_element_depth_value->setText(QString::asprintf("%.4f", m_element_depth->GetValue())); connect(m_element_depth, &ConfigFloatSlider::valueChanged, this, [this] { m_element_depth_value->setText(QString::asprintf("%.4f", m_element_depth->GetValue())); }); main_layout->addWidget(openxr_group); auto* tools_group = new QGroupBox(tr("Tools")); auto* tools_layout = new QGridLayout; tools_group->setLayout(tools_layout); auto* add_hide_object_code_btn = new QPushButton(tr("Add Hide Object Code")); connect(add_hide_object_code_btn, &QPushButton::clicked, this, [this] { const std::string game_id = SConfig::GetInstance().GetGameID(); if (game_id.empty() || game_id == "00000000") { QMessageBox::information(this, tr("No Game Running"), tr("Start a game before adding Hide Object codes.")); return; } auto codes = HideObjectEngine::LoadFromINI(game_id); HideObjectAddEditDialog dialog(this, nullptr, codes); if (dialog.exec() == QDialog::Accepted) { codes.push_back(dialog.GetResult()); HideObjectEngine::SaveToINI(game_id, codes); HideObjectEngine::Engine::GetInstance().ApplyCodes(codes); } else { // Restore the active code set if temporary brute-force values were applied. HideObjectEngine::Engine::GetInstance().ApplyCodes(codes); } }); tools_layout->addWidget(add_hide_object_code_btn, 0, 0); auto* shader_hunter_btn = new QPushButton(tr("Shader Hunter")); connect(shader_hunter_btn, &QPushButton::clicked, this, [this] { const std::string game_id = SConfig::GetInstance().GetGameID(); if (game_id.empty() || game_id == "00000000") { QMessageBox::information(this, tr("No Game Running"), tr("Start a game before searching for shaders.")); return; } if (m_shader_hunter_widget) { m_shader_hunter_widget->show(); m_shader_hunter_widget->raise(); m_shader_hunter_widget->activateWindow(); return; } m_shader_hunter_widget = new ShaderHunterWidget(this); m_shader_hunter_widget->setAttribute(Qt::WA_DeleteOnClose, true); m_shader_hunter_widget->show(); connect(m_shader_hunter_widget, &QObject::destroyed, this, [this] { m_shader_hunter_widget = nullptr; }); }); tools_layout->addWidget(shader_hunter_btn, 1, 0); m_load_custom_shaders = new ConfigBool(tr("Load Custom Shaders"), Config::GFX_VR_LOAD_CUSTOM_SHADERS); tools_layout->addWidget(m_load_custom_shaders, 2, 0); main_layout->addWidget(tools_group); main_layout->addStretch(); setLayout(main_layout); AddDescriptions(); connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, &VRPane::OnEmulationStateChanged); OnEmulationStateChanged(Core::GetState(Core::System::GetInstance())); } void VRPane::AddDescriptions() { static constexpr char TR_ENABLE_OPENXR_DESCRIPTION[] = QT_TR_NOOP( "Enables VR rendering through OpenXR." "

When enabled, Dolphin uses OpenXR instead of Stereoscopic 3D output modes." "

This setting cannot be changed while emulation is active." "

If unsure, leave this unchecked."); static constexpr char TR_UNITS_PER_METER_DESCRIPTION[] = QT_TR_NOOP( "Sets how many game world units correspond to one real-world meter." "

Higher values increase stereo scale and make the world appear smaller." "

Lower values decrease stereo scale and make the world appear larger."); static constexpr char TR_LEAN_BACK_ANGLE_DESCRIPTION[] = QT_TR_NOOP( "Applies a constant pitch offset (in degrees) to the VR camera." "

Use this to compensate games where the default viewpoint feels tilted." "

Positive values lean the camera back, negative values lean it forward."); static constexpr char TR_CAMERA_FORWARD_DESCRIPTION[] = QT_TR_NOOP( "Applies a constant forward/backward offset (in meters) to the VR camera." "

Positive values move the camera forward, negative values move it backward."); static constexpr char TR_VIRTUAL_SCREEN_DESCRIPTION[] = QT_TR_NOOP( "Places 2D content (menus, FMV, in-game HUD) on a virtual screen floating in 3D space." "

Disable for games that rely on full-screen EFB effects that don't work with a virtual screen."); static constexpr char TR_SCREEN_DISTANCE_DESCRIPTION[] = QT_TR_NOOP( "Distance in meters to the virtual screen used for 2D content (menus, FMV, HUD)." "

The screen is fixed in space like a TV — it stays in place when you turn your head."); static constexpr char TR_SCREEN_SIZE_DESCRIPTION[] = QT_TR_NOOP( "Height in meters of the virtual screen used for 2D content." "

Larger values make the screen bigger, smaller values make it more compact."); static constexpr char TR_HEAD_LOCKED_CURVATURE_DESCRIPTION[] = QT_TR_NOOP( "Curves the virtual screen used by Head Locked shader overrides." "

0 keeps the screen flat. Higher values increase curvature." "

This only affects shaders using Head Locked handling."); static constexpr char TR_DONT_CLEAR_SCREEN_DESCRIPTION[] = QT_TR_NOOP( "Prevents clearing the VR eye framebuffers before rendering each frame." "

By default the screen is cleared to black, which prevents image trails when " "looking outside the rendered area. Enable this if clearing causes visual glitches " "in specific games." "

If unsure, leave this unchecked."); static constexpr char TR_DISABLE_CPU_CULL_DESCRIPTION[] = QT_TR_NOOP( "Disables CPU-side primitive culling when OpenXR VR is active." "

This may fix missing geometry in some games at the cost of a small performance hit." "

This only affects Dolphin's CPU culling optimization. It does not override " "the game's own backface culling state."); static constexpr char TR_AUTO_VBI_FROM_HMD_DESCRIPTION[] = QT_TR_NOOP( "Forces Dolphin's VBI frequency to 90 Hz while OpenXR VR is enabled." "

This overrides the Advanced tab's VBI percentage during VR sessions." "

If unsure, leave this unchecked."); static constexpr char TR_AUTO_LAYER_SPREAD_DESCRIPTION[] = QT_TR_NOOP( "Automatically spreads 2D elements (menus, HUD, text) on the virtual screen into separate " "depth layers to prevent Z-fighting." "

Each ortho draw call gets a unique layer index, with later draws closer to the " "camera. Disable this if you prefer manual layer control via Shader Overrides." "

If unsure, leave this checked."); static constexpr char TR_LAYER_OFFSET_DESCRIPTION[] = QT_TR_NOOP( "Controls the depth separation between consecutive 2D layers on the virtual screen." "

Higher values spread layers further apart (stronger Z-fighting fix but may look " "wrong at extreme values). Lower values keep layers closer together." "

Default: 0.0020"); static constexpr char TR_ELEMENT_DEPTH_DESCRIPTION[] = QT_TR_NOOP( "Controls the depth range within individual 2D elements on the virtual screen." "

Higher values give more depth separation within each element, fixing Z-fighting " "that appears as flickering inside UI elements. Set to 0 to flatten elements completely." "

Default: 0.0010"); static constexpr char TR_CLEAR_EFB_COPIES_DESCRIPTION[] = QT_TR_NOOP( "Clears EFB (Embedded Framebuffer) copy textures to transparent when the copy's native " "width is at or above this threshold. Set to 0 to disable." "

Full-screen post-processing effects (bloom, blur) typically use full EFB width " "(640). HUD and small UI elements use smaller partial copies. Increasing this value " "preserves smaller EFB copies (like HUD textures) while removing large full-screen effects." "

Recommended: 400-640 to clear only full-screen effects, or 0 to disable." "

If unsure, leave this at 0 (off)."); static constexpr char TR_REMOVE_BARS_DESCRIPTION[] = QT_TR_NOOP( "Expands the scissor and viewport to fill the full active rendering area for perspective " "draws, removing cinematic letterbox bars." "

Some games (e.g. Metroid Prime) use GX scissor clipping to create cinematic " "bars that mask the top and bottom of the screen. In VR these bars are distracting. " "This option removes them by expanding the rendered area." "

Disable this if it causes visual artifacts in a specific game." "

If unsure, leave this checked."); static constexpr char TR_LOAD_CUSTOM_SHADERS_DESCRIPTION[] = QT_TR_NOOP( "Loads custom shader replacements from the Load/Shaders/<GameID>/ directory." "

Place edited shader files (dumped via Shader Hunter) in this folder to override " "the game's original shaders. Files must be named <hash>-<vs|ps|gs>.txt." "

If unsure, leave this unchecked."); m_enable_openxr->SetDescription(tr(TR_ENABLE_OPENXR_DESCRIPTION)); m_units_per_meter->SetDescription(tr(TR_UNITS_PER_METER_DESCRIPTION)); m_lean_back_angle->SetDescription(tr(TR_LEAN_BACK_ANGLE_DESCRIPTION)); m_camera_forward->SetDescription(tr(TR_CAMERA_FORWARD_DESCRIPTION)); m_virtual_screen->SetDescription(tr(TR_VIRTUAL_SCREEN_DESCRIPTION)); m_screen_distance->SetDescription(tr(TR_SCREEN_DISTANCE_DESCRIPTION)); m_screen_size->SetDescription(tr(TR_SCREEN_SIZE_DESCRIPTION)); m_head_locked_curvature->SetDescription(tr(TR_HEAD_LOCKED_CURVATURE_DESCRIPTION)); m_dont_clear_screen->SetDescription(tr(TR_DONT_CLEAR_SCREEN_DESCRIPTION)); m_disable_cpu_cull->SetDescription(tr(TR_DISABLE_CPU_CULL_DESCRIPTION)); m_auto_vbi_from_hmd->SetDescription(tr(TR_AUTO_VBI_FROM_HMD_DESCRIPTION)); m_clear_efb_slider->SetDescription(tr(TR_CLEAR_EFB_COPIES_DESCRIPTION)); m_remove_bars->SetDescription(tr(TR_REMOVE_BARS_DESCRIPTION)); m_auto_layer_spread->SetDescription(tr(TR_AUTO_LAYER_SPREAD_DESCRIPTION)); m_layer_offset->SetDescription(tr(TR_LAYER_OFFSET_DESCRIPTION)); m_element_depth->SetDescription(tr(TR_ELEMENT_DEPTH_DESCRIPTION)); m_load_custom_shaders->SetDescription(tr(TR_LOAD_CUSTOM_SHADERS_DESCRIPTION)); } void VRPane::OnEmulationStateChanged(Core::State state) { const bool running = state != Core::State::Uninitialized; m_enable_openxr->setEnabled(!running); }