mirror of
https://github.com/thejerf/baijiSteamVR.git
synced 2026-10-18 18:02:27 +02:00
710 lines
29 KiB
C++
710 lines
29 KiB
C++
// Copyright 2026 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "DolphinQt/Config/VRConfigWidget.h"
|
|
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <sstream>
|
|
#include <utility>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#include <QCheckBox>
|
|
#include <QFont>
|
|
#include <QGridLayout>
|
|
#include <QGroupBox>
|
|
#include <QLabel>
|
|
#include <QStyle>
|
|
#include <QTabWidget>
|
|
#include <QTimer>
|
|
#include <QVBoxLayout>
|
|
|
|
#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<u16> 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<bool> value = layer->Get<bool>(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<bool> value = layer->Get<bool>(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<u16> 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<u16> 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<u16> revision, QWidget* parent)
|
|
: QWidget(parent), m_game_id(std::move(game_id)), m_revision(revision),
|
|
m_layer(std::make_unique<Config::Layer>(
|
|
std::make_unique<VRGameConfigLayerLoader>(m_game_id, m_revision, false))),
|
|
m_global_layer(std::make_unique<Config::Layer>(
|
|
std::make_unique<VRGameConfigLayerLoader>(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<bool>& 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<float>& setting, float step, std::function<QString(float)> 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<int>& setting, int step,
|
|
std::function<QString(int)> 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<OpenXRPresentationMode>(
|
|
{{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<OpenXRMirrorView>(
|
|
{{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<int>({{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<int>({{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<int>(
|
|
{{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<VRPassthroughCoverageMode>(
|
|
{{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<OpenXRReferenceSpaceMode>(
|
|
{{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<OpenXRTrackingMode>(
|
|
{{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<int>(location.system));
|
|
state += ':';
|
|
append_part(location.section);
|
|
append_part(location.key);
|
|
state += value ? '1' : '0';
|
|
if (value)
|
|
append_part(*value);
|
|
}
|
|
return state;
|
|
}
|