Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
339a7ed52d | ||
|
|
220a99b7a3 | ||
|
|
49febf2cbe | ||
|
|
95b569b681 | ||
|
|
677aa34050 | ||
|
|
4a70fd2fbe | ||
|
|
1c16f73855 | ||
|
|
2ed53222b4 | ||
|
|
780b79b8d7 | ||
|
|
a2dad38be5 |
No files matched your search
@@ -93,6 +93,7 @@ option(ENABLE_CUBEB "Enables Cubeb sound backend" ON)
|
||||
option(ENABLE_LLVM "Enables LLVM support, for disassembly" ON)
|
||||
option(ENABLE_TESTS "Enables building the unit tests" ON)
|
||||
option(ENABLE_VULKAN "Enables vulkan video backend" ON)
|
||||
option(ENABLE_VR "Enables VR support via OpenXR (Windows, D3D11 + Vulkan)" OFF)
|
||||
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence, show the current game on Discord" ON)
|
||||
option(USE_MGBA "Enables GBA controllers emulation using libmgba" ON)
|
||||
option(ENABLE_AUTOUPDATE "Enables support for automatic updates" ON)
|
||||
@@ -653,6 +654,15 @@ if(ENABLE_VULKAN)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ENABLE_VR AND WIN32)
|
||||
add_definitions(-DENABLE_VR)
|
||||
# Build only the OpenXR loader from the Khronos SDK; skip tests, conformance, and samples.
|
||||
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_CONFORMANCE_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(Externals/OpenXR "${CMAKE_BINARY_DIR}/Externals/OpenXR" EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
if(NOT (WIN32 AND _M_ARM_64))
|
||||
# OpenGL is available on all platforms except windows-arm64
|
||||
add_definitions(-DHAS_OPENGL)
|
||||
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 963 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -3,6 +3,8 @@ set(MZ_BUILD_TESTS OFF)
|
||||
set(MZ_BUILD_UNIT_TESTS OFF)
|
||||
set(MZ_BUILD_FUZZ_TESTS OFF)
|
||||
set(MZ_CODE_COVERAGE OFF)
|
||||
set(MZ_FETCH_LIBS OFF CACHE BOOL "Disable minizip-ng vendored dependency fetch/build." FORCE)
|
||||
set(MZ_FORCE_FETCH_LIBS OFF CACHE BOOL "Disable minizip-ng forced vendored dependency fetch/build." FORCE)
|
||||
set(SKIP_INSTALL_ALL ON)
|
||||
|
||||
add_subdirectory(minizip-ng)
|
||||
|
||||
@@ -6,6 +6,9 @@ Dolphin is an emulator for running GameCube and Wii games on Windows,
|
||||
Linux, macOS, and recent Android devices. It's licensed under the terms
|
||||
of the GNU General Public License, version 2 or later (GPLv2+).
|
||||
|
||||
This fork adds OpenXR support to Dolphin so users can play in VR on supported
|
||||
Windows builds and was developed with the use of AI tools.
|
||||
|
||||
Please read the [FAQ](https://dolphin-emu.org/docs/faq/) before using Dolphin.
|
||||
|
||||
## System Requirements
|
||||
@@ -49,6 +52,30 @@ Make sure to pull submodules before building:
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### Building the OpenXR VR version on Windows
|
||||
|
||||
To build the VR-enabled version of this fork, OpenXR support must be enabled and
|
||||
the OpenXR loader binaries must be compiled before building Dolphin itself.
|
||||
|
||||
If you are generating a Visual Studio build with CMake, configure with
|
||||
`-DENABLE_VR=ON`:
|
||||
```sh
|
||||
cmake -S . -B Build-vs2022 -G "Visual Studio 17 2022" -A x64 -DENABLE_VR=ON
|
||||
```
|
||||
|
||||
Then build the OpenXR loader first:
|
||||
```sh
|
||||
cmake --build Build-vs2022 --config Release --target openxr_loader
|
||||
```
|
||||
|
||||
Finally, build Dolphin:
|
||||
```sh
|
||||
cmake --build Build-vs2022 --config Release --target dolphin-emu
|
||||
```
|
||||
|
||||
If you prefer building OpenXR separately first, the repository also includes
|
||||
`Externals/OpenXR/build-win64/OPENXR.sln`.
|
||||
|
||||
The "Release" solution configuration includes performance optimizations for the best user experience but complicates debugging Dolphin.
|
||||
The "Debug" solution configuration is significantly slower, more verbose and less permissive but makes debugging Dolphin easier.
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#define TRI_USER_DIR "Triforce"
|
||||
#define CONFIG_DIR "Config"
|
||||
#define GAMESETTINGS_DIR "GameSettings"
|
||||
#define GAMESETTINGSVR_DIR "GameSettingsVR"
|
||||
#define MAPS_DIR "Maps"
|
||||
#define CACHE_DIR "Cache"
|
||||
#define COVERCACHE_DIR "GameCovers"
|
||||
|
||||
@@ -841,6 +841,8 @@ static void RebuildUserDirectories(unsigned int dir_index)
|
||||
s_user_paths[D_TRIUSER_IDX] = s_user_paths[D_USER_IDX] + TRI_USER_DIR DIR_SEP;
|
||||
s_user_paths[D_CONFIG_IDX] = s_user_paths[D_USER_IDX] + CONFIG_DIR DIR_SEP;
|
||||
s_user_paths[D_GAMESETTINGS_IDX] = s_user_paths[D_USER_IDX] + GAMESETTINGS_DIR DIR_SEP;
|
||||
s_user_paths[D_GAMESETTINGSVR_IDX] =
|
||||
s_user_paths[D_USER_IDX] + GAMESETTINGSVR_DIR DIR_SEP;
|
||||
s_user_paths[D_MAPS_IDX] = s_user_paths[D_USER_IDX] + MAPS_DIR DIR_SEP;
|
||||
s_user_paths[D_CACHE_IDX] = s_user_paths[D_USER_IDX] + CACHE_DIR DIR_SEP;
|
||||
s_user_paths[D_COVERCACHE_IDX] = s_user_paths[D_CACHE_IDX] + COVERCACHE_DIR DIR_SEP;
|
||||
|
||||
@@ -32,6 +32,7 @@ enum
|
||||
D_CONFIG_IDX, // global settings
|
||||
D_GAMESETTINGS_IDX, // user-specified settings which override both the global and the default
|
||||
// settings (per game)
|
||||
D_GAMESETTINGSVR_IDX, // per-game VR settings/shader override INIs
|
||||
D_SKYLANDERS_IDX,
|
||||
|
||||
D_MAPS_IDX,
|
||||
|
||||
@@ -66,6 +66,7 @@ enum class LogType : int
|
||||
SP1,
|
||||
SYMBOLS,
|
||||
VIDEO,
|
||||
OPENXR,
|
||||
VIDEOINTERFACE,
|
||||
WII_IPC,
|
||||
WIIMOTE,
|
||||
|
||||
@@ -150,6 +150,7 @@ LogManager::LogManager()
|
||||
m_log[LogType::SP1] = {"SP1", "Serial Port 1"};
|
||||
m_log[LogType::SYMBOLS] = {"SYMBOLS", "Symbols"};
|
||||
m_log[LogType::VIDEO] = {"Video", "Video Backend"};
|
||||
m_log[LogType::OPENXR] = {"OpenXR", "OpenXR"};
|
||||
m_log[LogType::VIDEOINTERFACE] = {"VI", "Video Interface"};
|
||||
m_log[LogType::WIIMOTE] = {"Wiimote", "Wii Remote"};
|
||||
m_log[LogType::WII_IPC] = {"WII_IPC", "WII IPC"};
|
||||
|
||||
@@ -54,6 +54,9 @@ const Info<bool> GFX_LOG_RENDER_TIME_TO_FILE{{System::GFX, "Settings", "LogRende
|
||||
const Info<bool> GFX_OVERLAY_STATS{{System::GFX, "Settings", "OverlayStats"}, false};
|
||||
const Info<bool> GFX_OVERLAY_PROJ_STATS{{System::GFX, "Settings", "OverlayProjStats"}, false};
|
||||
const Info<bool> GFX_OVERLAY_SCISSOR_STATS{{System::GFX, "Settings", "OverlayScissorStats"}, false};
|
||||
const Info<bool> GFX_OVERLAY_SHADER_FLAGS{{System::GFX, "Settings", "OverlayShaderFlags"}, false};
|
||||
const Info<bool> GFX_OVERLAY_SHADER_HUNTING{{System::GFX, "Settings", "OverlayShaderHunting"},
|
||||
false};
|
||||
const Info<bool> GFX_DUMP_TEXTURES{{System::GFX, "Settings", "DumpTextures"}, false};
|
||||
const Info<bool> GFX_DUMP_MIP_TEXTURES{{System::GFX, "Settings", "DumpMipTextures"}, true};
|
||||
const Info<bool> GFX_DUMP_BASE_TEXTURES{{System::GFX, "Settings", "DumpBaseTextures"}, true};
|
||||
@@ -180,6 +183,35 @@ const Info<bool> GFX_STEREO_EFB_MONO_DEPTH{{System::GFX, "Stereoscopy", "StereoE
|
||||
const Info<float> GFX_STEREO_DEPTH_PERCENTAGE{{System::GFX, "Stereoscopy", "StereoDepthPercentage"},
|
||||
100};
|
||||
|
||||
// Graphics.VR
|
||||
|
||||
const Info<bool> GFX_VR_ENABLE_OPENXR{{System::GFX, "VR", "EnableOpenXR"}, false};
|
||||
const Info<bool> GFX_VR_AUTO_IMMEDIATE_XFB{{System::GFX, "VR", "AutoImmediateXFB"}, true};
|
||||
const Info<float> GFX_VR_UNITS_PER_METER{{System::GFX, "VR", "UnitsPerMeter"}, 1.0f};
|
||||
const Info<float> GFX_VR_LEAN_BACK_ANGLE{{System::GFX, "VR", "LeanBackAngle"}, 0.0f};
|
||||
const Info<float> GFX_VR_CAMERA_FORWARD{{System::GFX, "VR", "CameraForward"}, 0.0f};
|
||||
const Info<bool> GFX_VR_VIRTUAL_SCREEN{{System::GFX, "VR", "VirtualScreen"}, true};
|
||||
const Info<float> GFX_VR_SCREEN_DISTANCE{{System::GFX, "VR", "ScreenDistance"}, 1.5f};
|
||||
const Info<float> GFX_VR_SCREEN_SIZE{{System::GFX, "VR", "ScreenSize"}, 1.5f};
|
||||
const Info<float> GFX_VR_HEAD_LOCKED_CURVATURE{{System::GFX, "VR", "HeadLockedCurvature"}, 0.0f};
|
||||
const Info<bool> GFX_VR_DONT_CLEAR_SCREEN{{System::GFX, "VR", "DontClearScreen"}, false};
|
||||
const Info<bool> GFX_VR_LOAD_CUSTOM_SHADERS{{System::GFX, "VR", "LoadCustomShaders"}, false};
|
||||
const Info<bool> GFX_VR_ENABLE_OPENXR_CONFIG_SCENE{{System::GFX, "VR", "EnableOpenXRConfigScene"},
|
||||
true};
|
||||
const Info<bool> GFX_VR_DISABLE_CPU_CULL{{System::GFX, "VR", "DisableCPUCull"}, false};
|
||||
const Info<OpenXROpcodeReplayMode> GFX_VR_OPCODE_REPLAY{
|
||||
{System::GFX, "VR", "OpcodeReplay"}, OpenXROpcodeReplayMode::Off};
|
||||
const Info<bool> GFX_VR_AUTO_VBI_FROM_HMD{{System::GFX, "VR", "AutoVBIFromHMD"}, false};
|
||||
const Info<bool> GFX_VR_AUTO_LAYER_SPREAD{{System::GFX, "VR", "AutoLayerSpread"}, true};
|
||||
const Info<float> GFX_VR_LAYER_OFFSET{{System::GFX, "VR", "LayerOffset"}, 0.002f};
|
||||
const Info<float> GFX_VR_ELEMENT_DEPTH{{System::GFX, "VR", "ElementDepth"}, 0.001f};
|
||||
const Info<bool> GFX_VR_REMOVE_BARS{{System::GFX, "VR", "RemoveCinematicBars"}, true};
|
||||
const Info<bool> GFX_VR_LOCK_HEAD_POSE{{System::GFX, "VR", "LockHeadPosePerFrame"}, true};
|
||||
const Info<bool> GFX_VR_AR_MODE{{System::GFX, "VR", "ARMode"}, false};
|
||||
const Info<bool> GFX_VR_AR_MODE_DEBUG{{System::GFX, "VR", "ARModeDebug"}, false};
|
||||
const Info<float> GFX_VR_AR_BACKGROUND_ALPHA{{System::GFX, "VR", "ARBackgroundAlpha"}, 0.0f};
|
||||
const Info<float> GFX_VR_GAMMA{{System::GFX, "VR", "Gamma"}, 1.0f};
|
||||
const Info<int> GFX_VR_CLEAR_EFB_COPIES{{System::GFX, "VR", "ClearEFBCopies"}, 0};
|
||||
// Graphics.Hacks
|
||||
|
||||
const Info<bool> GFX_HACK_EFB_ACCESS_ENABLE{{System::GFX, "Hacks", "EFBAccessEnable"}, false};
|
||||
|
||||
@@ -18,6 +18,7 @@ enum class ColorCorrectionRegion : int;
|
||||
enum class TriState : int;
|
||||
enum class FrameDumpResolutionType : int;
|
||||
enum class VertexLoaderType : int;
|
||||
enum class OpenXROpcodeReplayMode : int;
|
||||
|
||||
namespace Config
|
||||
{
|
||||
@@ -56,6 +57,8 @@ extern const Info<bool> GFX_LOG_RENDER_TIME_TO_FILE;
|
||||
extern const Info<bool> GFX_OVERLAY_STATS;
|
||||
extern const Info<bool> GFX_OVERLAY_PROJ_STATS;
|
||||
extern const Info<bool> GFX_OVERLAY_SCISSOR_STATS;
|
||||
extern const Info<bool> GFX_OVERLAY_SHADER_FLAGS;
|
||||
extern const Info<bool> GFX_OVERLAY_SHADER_HUNTING;
|
||||
extern const Info<bool> GFX_DUMP_TEXTURES;
|
||||
extern const Info<bool> GFX_DUMP_MIP_TEXTURES;
|
||||
extern const Info<bool> GFX_DUMP_BASE_TEXTURES;
|
||||
@@ -156,6 +159,70 @@ extern const Info<float> GFX_STEREO_DEPTH_PERCENTAGE;
|
||||
static constexpr float GFX_STEREO_DEPTH_MAXIMUM = 100;
|
||||
static constexpr float GFX_STEREO_CONVERGENCE_MAXIMUM = 200;
|
||||
|
||||
// Graphics.VR
|
||||
|
||||
extern const Info<bool> GFX_VR_ENABLE_OPENXR;
|
||||
extern const Info<bool> GFX_VR_AUTO_IMMEDIATE_XFB;
|
||||
extern const Info<float> GFX_VR_UNITS_PER_METER;
|
||||
extern const Info<float> GFX_VR_LEAN_BACK_ANGLE;
|
||||
extern const Info<float> GFX_VR_CAMERA_FORWARD;
|
||||
|
||||
static constexpr float GFX_VR_UNITS_PER_METER_MIN = 0.1f;
|
||||
static constexpr float GFX_VR_UNITS_PER_METER_MAX = 500.0f;
|
||||
static constexpr float GFX_VR_UNITS_PER_METER_STEP = 0.1f;
|
||||
static constexpr float GFX_VR_LEAN_BACK_ANGLE_MIN = -45.0f;
|
||||
static constexpr float GFX_VR_LEAN_BACK_ANGLE_MAX = 45.0f;
|
||||
static constexpr float GFX_VR_LEAN_BACK_ANGLE_STEP = 0.1f;
|
||||
static constexpr float GFX_VR_CAMERA_FORWARD_MIN = -20.0f;
|
||||
static constexpr float GFX_VR_CAMERA_FORWARD_MAX = 20.0f;
|
||||
static constexpr float GFX_VR_CAMERA_FORWARD_STEP = 0.1f;
|
||||
|
||||
extern const Info<bool> GFX_VR_VIRTUAL_SCREEN;
|
||||
extern const Info<float> GFX_VR_SCREEN_DISTANCE;
|
||||
extern const Info<float> GFX_VR_SCREEN_SIZE;
|
||||
extern const Info<float> GFX_VR_HEAD_LOCKED_CURVATURE;
|
||||
extern const Info<bool> GFX_VR_DONT_CLEAR_SCREEN;
|
||||
extern const Info<bool> GFX_VR_LOAD_CUSTOM_SHADERS;
|
||||
extern const Info<bool> GFX_VR_ENABLE_OPENXR_CONFIG_SCENE;
|
||||
extern const Info<bool> GFX_VR_DISABLE_CPU_CULL;
|
||||
extern const Info<OpenXROpcodeReplayMode> GFX_VR_OPCODE_REPLAY;
|
||||
extern const Info<bool> GFX_VR_AUTO_VBI_FROM_HMD;
|
||||
extern const Info<bool> GFX_VR_AUTO_LAYER_SPREAD;
|
||||
extern const Info<float> GFX_VR_LAYER_OFFSET;
|
||||
extern const Info<float> GFX_VR_ELEMENT_DEPTH;
|
||||
extern const Info<bool> GFX_VR_REMOVE_BARS;
|
||||
extern const Info<bool> GFX_VR_LOCK_HEAD_POSE;
|
||||
extern const Info<bool> GFX_VR_AR_MODE;
|
||||
extern const Info<bool> GFX_VR_AR_MODE_DEBUG;
|
||||
extern const Info<float> GFX_VR_AR_BACKGROUND_ALPHA;
|
||||
static constexpr float GFX_VR_AR_BACKGROUND_ALPHA_MIN = 0.0f;
|
||||
static constexpr float GFX_VR_AR_BACKGROUND_ALPHA_MAX = 1.0f;
|
||||
static constexpr float GFX_VR_AR_BACKGROUND_ALPHA_STEP = 0.05f;
|
||||
extern const Info<float> GFX_VR_GAMMA;
|
||||
extern const Info<int> GFX_VR_CLEAR_EFB_COPIES;
|
||||
static constexpr int GFX_VR_CLEAR_EFB_MIN = 0;
|
||||
static constexpr int GFX_VR_CLEAR_EFB_MAX = 640;
|
||||
static constexpr int GFX_VR_CLEAR_EFB_STEP = 10;
|
||||
|
||||
static constexpr float GFX_VR_SCREEN_DISTANCE_MIN = 0.5f;
|
||||
static constexpr float GFX_VR_SCREEN_DISTANCE_MAX = 10.0f;
|
||||
static constexpr float GFX_VR_SCREEN_DISTANCE_STEP = 0.1f;
|
||||
static constexpr float GFX_VR_SCREEN_SIZE_MIN = 0.5f;
|
||||
static constexpr float GFX_VR_SCREEN_SIZE_MAX = 5.0f;
|
||||
static constexpr float GFX_VR_SCREEN_SIZE_STEP = 0.1f;
|
||||
static constexpr float GFX_VR_HEAD_LOCKED_CURVATURE_MIN = 0.0f;
|
||||
static constexpr float GFX_VR_HEAD_LOCKED_CURVATURE_MAX = 5.0f;
|
||||
static constexpr float GFX_VR_HEAD_LOCKED_CURVATURE_STEP = 0.01f;
|
||||
static constexpr float GFX_VR_LAYER_OFFSET_MIN = 0.0001f;
|
||||
static constexpr float GFX_VR_LAYER_OFFSET_MAX = 0.01f;
|
||||
static constexpr float GFX_VR_LAYER_OFFSET_STEP = 0.0001f;
|
||||
static constexpr float GFX_VR_ELEMENT_DEPTH_MIN = 0.0f;
|
||||
static constexpr float GFX_VR_ELEMENT_DEPTH_MAX = 0.1f;
|
||||
static constexpr float GFX_VR_ELEMENT_DEPTH_STEP = 0.0001f;
|
||||
static constexpr float GFX_VR_GAMMA_MIN = 1.0f;
|
||||
static constexpr float GFX_VR_GAMMA_MAX = 3.0f;
|
||||
static constexpr float GFX_VR_GAMMA_STEP = 0.1f;
|
||||
|
||||
// Graphics.Hacks
|
||||
|
||||
extern const Info<bool> GFX_HACK_EFB_ACCESS_ENABLE;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "Core/ConfigManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -11,8 +13,6 @@
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
#include <Core/Core.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "AudioCommon/AudioCommon.h"
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "Core/AchievementManager.h"
|
||||
#include "Core/Boot/Boot.h"
|
||||
#include "Core/Config/DefaultLocale.h"
|
||||
#include "Core/Config/GraphicsSettings.h"
|
||||
#include "Core/Config/MainSettings.h"
|
||||
#include "Core/Config/SYSCONFSettings.h"
|
||||
#include "Core/ConfigLoaders/GameConfigLoader.h"
|
||||
@@ -58,6 +59,124 @@
|
||||
#include "DiscIO/Volume.h"
|
||||
#include "DiscIO/VolumeWad.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view VR_SECTION_NAME = "Graphics.VR";
|
||||
constexpr std::string_view LEGACY_VR_SECTION_NAME = "GFX.VR";
|
||||
|
||||
using VRSettingMap = std::map<std::string, std::string, Common::CaseInsensitiveLess>;
|
||||
|
||||
static std::string GetVRGameINIPath(std::string_view game_id)
|
||||
{
|
||||
return File::GetUserPath(D_GAMESETTINGSVR_IDX) + std::string(game_id) + ".ini";
|
||||
}
|
||||
|
||||
static bool IsVRSectionHeader(std::string_view line)
|
||||
{
|
||||
if (line.size() < 3 || line.front() != '[' || line.back() != ']')
|
||||
return false;
|
||||
|
||||
const std::string_view section_name = line.substr(1, line.size() - 2);
|
||||
return Common::CaseInsensitiveEquals(section_name, VR_SECTION_NAME) ||
|
||||
Common::CaseInsensitiveEquals(section_name, LEGACY_VR_SECTION_NAME);
|
||||
}
|
||||
|
||||
static VRSettingMap LoadVRSettingsFromINI(std::string_view game_id)
|
||||
{
|
||||
VRSettingMap values;
|
||||
|
||||
if (game_id.empty() || game_id == DEFAULT_GAME_ID)
|
||||
return values;
|
||||
|
||||
std::ifstream file(GetVRGameINIPath(game_id));
|
||||
if (!file.is_open())
|
||||
return values;
|
||||
|
||||
bool in_vr_section = false;
|
||||
std::string line;
|
||||
while (std::getline(file, line))
|
||||
{
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line.pop_back();
|
||||
|
||||
const std::string_view trimmed = StripWhitespace(line);
|
||||
if (trimmed.empty())
|
||||
continue;
|
||||
|
||||
if (!trimmed.empty() && trimmed.front() == '[')
|
||||
{
|
||||
if (IsVRSectionHeader(trimmed))
|
||||
{
|
||||
in_vr_section = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_vr_section)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!in_vr_section)
|
||||
continue;
|
||||
|
||||
if (trimmed.front() == '#' || trimmed.front() == ';' || trimmed.front() == '$' ||
|
||||
trimmed.front() == '*')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string key;
|
||||
std::string value;
|
||||
Common::IniFile::ParseLine(trimmed, &key, &value);
|
||||
if (!key.empty())
|
||||
values.insert_or_assign(std::move(key), std::move(value));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void ApplyVRSetting(const VRSettingMap& values, const char* key, const Config::Info<T>& info)
|
||||
{
|
||||
const auto it = values.find(key);
|
||||
if (it == values.end())
|
||||
{
|
||||
Config::DeleteKey(Config::LayerType::CurrentRun, info);
|
||||
return;
|
||||
}
|
||||
|
||||
T parsed_value{};
|
||||
if (TryParse(it->second, &parsed_value))
|
||||
Config::SetCurrent(info, parsed_value);
|
||||
else
|
||||
Config::DeleteKey(Config::LayerType::CurrentRun, info);
|
||||
}
|
||||
|
||||
static void ApplyGameVRConfigOverrides(std::string_view game_id)
|
||||
{
|
||||
const Config::ConfigChangeCallbackGuard guard;
|
||||
const VRSettingMap values = LoadVRSettingsFromINI(game_id);
|
||||
|
||||
ApplyVRSetting(values, "EnableOpenXR", Config::GFX_VR_ENABLE_OPENXR);
|
||||
ApplyVRSetting(values, "UnitsPerMeter", Config::GFX_VR_UNITS_PER_METER);
|
||||
ApplyVRSetting(values, "LeanBackAngle", Config::GFX_VR_LEAN_BACK_ANGLE);
|
||||
ApplyVRSetting(values, "CameraForward", Config::GFX_VR_CAMERA_FORWARD);
|
||||
ApplyVRSetting(values, "VirtualScreen", Config::GFX_VR_VIRTUAL_SCREEN);
|
||||
ApplyVRSetting(values, "ScreenDistance", Config::GFX_VR_SCREEN_DISTANCE);
|
||||
ApplyVRSetting(values, "ScreenSize", Config::GFX_VR_SCREEN_SIZE);
|
||||
ApplyVRSetting(values, "DontClearScreen", Config::GFX_VR_DONT_CLEAR_SCREEN);
|
||||
ApplyVRSetting(values, "LoadCustomShaders", Config::GFX_VR_LOAD_CUSTOM_SHADERS);
|
||||
ApplyVRSetting(values, "DisableCPUCull", Config::GFX_VR_DISABLE_CPU_CULL);
|
||||
ApplyVRSetting(values, "AutoVBIFromHMD", Config::GFX_VR_AUTO_VBI_FROM_HMD);
|
||||
ApplyVRSetting(values, "AutoLayerSpread", Config::GFX_VR_AUTO_LAYER_SPREAD);
|
||||
ApplyVRSetting(values, "LayerOffset", Config::GFX_VR_LAYER_OFFSET);
|
||||
ApplyVRSetting(values, "ElementDepth", Config::GFX_VR_ELEMENT_DEPTH);
|
||||
ApplyVRSetting(values, "ClearEFBCopies", Config::GFX_VR_CLEAR_EFB_COPIES);
|
||||
ApplyVRSetting(values, "ARMode", Config::GFX_VR_AR_MODE);
|
||||
ApplyVRSetting(values, "ARModeDebug", Config::GFX_VR_AR_MODE_DEBUG);
|
||||
ApplyVRSetting(values, "ARBackgroundAlpha", Config::GFX_VR_AR_BACKGROUND_ALPHA);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SConfig* SConfig::m_Instance;
|
||||
|
||||
SConfig::SConfig()
|
||||
@@ -253,11 +372,18 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri
|
||||
|
||||
Config::AddLayer(ConfigLoaders::GenerateGlobalGameConfigLoader(game_id, revision));
|
||||
Config::AddLayer(ConfigLoaders::GenerateLocalGameConfigLoader(game_id, revision));
|
||||
ApplyGameVRConfigOverrides(game_id);
|
||||
|
||||
if (is_running_or_starting)
|
||||
DolphinAnalytics::Instance().ReportGameStart();
|
||||
}
|
||||
|
||||
void SConfig::ReloadGameVRConfigOverrides()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
|
||||
ApplyGameVRConfigOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void SConfig::OnESTitleChanged()
|
||||
{
|
||||
auto& system = Core::System::GetInstance();
|
||||
|
||||
@@ -89,6 +89,7 @@ struct SConfig
|
||||
Common::IniFile LoadDefaultGameIni() const;
|
||||
Common::IniFile LoadLocalGameIni() const;
|
||||
Common::IniFile LoadGameIni() const;
|
||||
void ReloadGameVRConfigOverrides();
|
||||
|
||||
static Common::IniFile LoadDefaultGameIni(std::string_view id, std::optional<u16> revision);
|
||||
static Common::IniFile LoadLocalGameIni(std::string_view id, std::optional<u16> revision);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "Core/AchievementManager.h"
|
||||
#include "Core/CPUThreadConfigCallback.h"
|
||||
#include "Core/Config/GraphicsSettings.h"
|
||||
#include "Core/Config/MainSettings.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/SystemTimers.h"
|
||||
@@ -35,6 +36,24 @@
|
||||
namespace CoreTiming
|
||||
{
|
||||
static constexpr int MAX_SLICE_LENGTH = 20000;
|
||||
static constexpr float GC_BASE_VBI_FREQUENCY_HZ = 59.94f;
|
||||
static constexpr float VR_FORCED_VBI_FREQUENCY_HZ = 90.0f;
|
||||
|
||||
static float GetEffectiveVIOverclockFactor()
|
||||
{
|
||||
float vi_factor = Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_VI_OVERCLOCK) :
|
||||
1.0f;
|
||||
if (Config::Get(Config::GFX_VR_AUTO_VBI_FROM_HMD) && Config::Get(Config::GFX_VR_ENABLE_OPENXR))
|
||||
vi_factor = VR_FORCED_VBI_FREQUENCY_HZ / GC_BASE_VBI_FREQUENCY_HZ;
|
||||
return std::max(vi_factor, 0.01f);
|
||||
}
|
||||
|
||||
static float GetEffectiveTotalOverclockFactor()
|
||||
{
|
||||
const float cpu_factor =
|
||||
Config::Get(Config::MAIN_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_OVERCLOCK) : 1.0f;
|
||||
return cpu_factor * GetEffectiveVIOverclockFactor();
|
||||
}
|
||||
|
||||
static void EmptyTimedCallback(Core::System& system, u64 userdata, s64 cyclesLate)
|
||||
{
|
||||
@@ -136,10 +155,7 @@ void CoreTimingManager::Shutdown()
|
||||
|
||||
void CoreTimingManager::RefreshConfig()
|
||||
{
|
||||
m_config_oc_factor =
|
||||
(Config::Get(Config::MAIN_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_OVERCLOCK) : 1.0f) *
|
||||
(Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_VI_OVERCLOCK) :
|
||||
1.0f);
|
||||
m_config_oc_factor = GetEffectiveTotalOverclockFactor();
|
||||
m_config_oc_inv_factor = 1.0f / m_config_oc_factor;
|
||||
m_config_sync_on_skip_idle = Config::Get(Config::MAIN_SYNC_ON_SKIP_IDLE);
|
||||
m_config_rush_frame_presentation = Config::Get(Config::MAIN_RUSH_FRAME_PRESENTATION);
|
||||
|
||||
@@ -54,6 +54,17 @@ static constexpr std::array<u32, 2> CLOCK_FREQUENCIES{{
|
||||
}};
|
||||
|
||||
static constexpr u32 NUM_HALF_LINES_FOR_SI_POLL = (7 * 2) + 1; // this is how long an SI poll takes
|
||||
static constexpr float GC_BASE_VBI_FREQUENCY_HZ = 59.94f;
|
||||
static constexpr float VR_FORCED_VBI_FREQUENCY_HZ = 90.0f;
|
||||
|
||||
static float GetEffectiveVBIOverrideFactor()
|
||||
{
|
||||
float factor = Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_VI_OVERCLOCK) :
|
||||
1.0f;
|
||||
if (Config::Get(Config::GFX_VR_AUTO_VBI_FROM_HMD) && Config::Get(Config::GFX_VR_ENABLE_OPENXR))
|
||||
factor = VR_FORCED_VBI_FREQUENCY_HZ / GC_BASE_VBI_FREQUENCY_HZ;
|
||||
return std::max(factor, 0.01f);
|
||||
}
|
||||
|
||||
void VideoInterfaceManager::DoState(PointerWrap& p)
|
||||
{
|
||||
@@ -187,8 +198,7 @@ void VideoInterfaceManager::Init()
|
||||
|
||||
void VideoInterfaceManager::RefreshConfig()
|
||||
{
|
||||
m_config_vi_oc_factor =
|
||||
Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE) ? Config::Get(Config::MAIN_VI_OVERCLOCK) : 1.0f;
|
||||
m_config_vi_oc_factor = GetEffectiveVBIOverrideFactor();
|
||||
if (AchievementManager::GetInstance().IsHardcoreModeActive() && m_config_vi_oc_factor < 1.0f)
|
||||
{
|
||||
Config::SetCurrent(Config::MAIN_VI_OVERCLOCK, 1.0f);
|
||||
|
||||
@@ -3,16 +3,26 @@
|
||||
|
||||
#include "Core/HW/Wiimote.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Config/Config.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/MathUtil.h"
|
||||
#include "Common/Matrix.h"
|
||||
#ifdef ENABLE_VR
|
||||
#include "Common/VR/OpenXRInputState.h"
|
||||
#endif
|
||||
|
||||
#include "Core/Config/WiimoteSettings.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
|
||||
#include "Core/HW/WiimoteEmu/Camera.h"
|
||||
#include "Core/HW/WiimoteReal/WiimoteReal.h"
|
||||
#include "Core/IOS/IOS.h"
|
||||
#include "Core/IOS/USB/Bluetooth/BTEmu.h"
|
||||
@@ -22,6 +32,9 @@
|
||||
#include "Core/WiiUtils.h"
|
||||
|
||||
#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
|
||||
#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h"
|
||||
#include "InputCommon/ControllerEmu/ControlGroup/Attachments.h"
|
||||
#include "InputCommon/ControllerEmu/StickGate.h"
|
||||
#include "InputCommon/InputConfig.h"
|
||||
|
||||
// Limit the amount of wiimote connect requests, when a button is pressed in disconnected state
|
||||
@@ -32,6 +45,401 @@ namespace
|
||||
static std::array<std::atomic<WiimoteSource>, MAX_BBMOTES> s_wiimote_sources;
|
||||
static std::optional<Config::ConfigChangedCallbackID> s_config_callback_id = std::nullopt;
|
||||
|
||||
bool IsEmulatedSource(WiimoteSource source)
|
||||
{
|
||||
return source == WiimoteSource::Emulated || source == WiimoteSource::OpenXR;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_VR
|
||||
Common::Quaternion ToQuaternion(const std::array<float, 4>& quat)
|
||||
{
|
||||
return {quat[3], quat[0], quat[1], quat[2]};
|
||||
}
|
||||
|
||||
Common::Vec3 ToVec3(const std::array<float, 3>& vec)
|
||||
{
|
||||
return {vec[0], vec[1], vec[2]};
|
||||
}
|
||||
|
||||
struct OpenXRVelocityHistory
|
||||
{
|
||||
bool has_pose_sample = false;
|
||||
bool has_velocity_sample = false;
|
||||
Common::Vec3 previous_position{};
|
||||
Common::Vec3 previous_velocity{};
|
||||
std::chrono::steady_clock::time_point previous_time{};
|
||||
};
|
||||
|
||||
struct OpenXRWiimoteState
|
||||
{
|
||||
u64 generation = std::numeric_limits<u64>::max();
|
||||
Common::Vec3 acceleration{0.0f, 0.0f, float(MathUtil::GRAVITY_ACCELERATION)};
|
||||
Common::Vec3 angular_velocity{};
|
||||
float ir_x = std::numeric_limits<float>::quiet_NaN();
|
||||
float ir_y = 0.0f;
|
||||
float ir_z = 0.0f; // Forward/backward distance offset (-1 to +1)
|
||||
// Raw aim orientation for reference-based IR computation in ApplyOpenXRIRCenter
|
||||
Common::Quaternion aim_orientation{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
Common::Vec3 aim_position{}; // Aim pose world position for Z computation
|
||||
bool has_aim = false;
|
||||
};
|
||||
|
||||
struct OpenXRIRCenter
|
||||
{
|
||||
bool initialized = false;
|
||||
// Reference orientation from the HMD — "where the user is looking" becomes cursor center.
|
||||
// Controller aim deviation from this direction maps to cursor position.
|
||||
Common::Quaternion reference_orientation;
|
||||
Common::Vec3 reference_position{}; // Controller reference position for forward/backward
|
||||
};
|
||||
|
||||
struct OpenXRCursorRanges
|
||||
{
|
||||
float yaw_half_range = float(MathUtil::PI * 25.0 / 180.0 / 2.0);
|
||||
float pitch_half_range = float(MathUtil::PI * 20.0 / 180.0 / 2.0);
|
||||
};
|
||||
|
||||
OpenXRCursorRanges GetOpenXRCursorRanges(unsigned int wiimote_index)
|
||||
{
|
||||
OpenXRCursorRanges ranges;
|
||||
|
||||
if (wiimote_index >= MAX_WIIMOTES)
|
||||
return ranges;
|
||||
|
||||
auto* config = ::Wiimote::GetConfig();
|
||||
if (!config)
|
||||
return ranges;
|
||||
|
||||
auto* wiimote = static_cast<WiimoteEmu::Wiimote*>(config->GetController(wiimote_index));
|
||||
if (!wiimote)
|
||||
return ranges;
|
||||
|
||||
auto* ir_group = static_cast<ControllerEmu::Cursor*>(
|
||||
wiimote->GetWiimoteGroup(WiimoteEmu::WiimoteGroup::Point));
|
||||
if (!ir_group)
|
||||
return ranges;
|
||||
|
||||
ranges.yaw_half_range = std::max(static_cast<float>(ir_group->GetTotalYaw() / 2), 0.001f);
|
||||
ranges.pitch_half_range = std::max(static_cast<float>(ir_group->GetTotalPitch() / 2), 0.001f);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
OpenXRWiimoteState BuildOpenXRState(const Common::VR::OpenXRControllerState& controller,
|
||||
OpenXRVelocityHistory* velocity_history)
|
||||
{
|
||||
OpenXRWiimoteState out;
|
||||
|
||||
const bool has_grip_pose = controller.grip_pose.valid;
|
||||
const bool has_aim_pose = controller.aim_pose.valid;
|
||||
if (!has_grip_pose && !has_aim_pose)
|
||||
return out;
|
||||
|
||||
// Use aim pose for the motion reference frame. The aim pose orientation determines how
|
||||
// world-space gravity maps to Wiimote-local accelerometer axes. Using grip would produce
|
||||
// wrong accel values because grip/aim can differ by 30-60 degrees on many controllers.
|
||||
const Common::Quaternion reference_orientation =
|
||||
(has_aim_pose ? ToQuaternion(controller.aim_pose.orientation) :
|
||||
ToQuaternion(controller.grip_pose.orientation))
|
||||
.Normalized();
|
||||
const Common::Matrix33 world_to_local =
|
||||
Common::Matrix33::FromQuaternion(reference_orientation).Transposed();
|
||||
|
||||
const auto get_matrix = [&world_to_local](int row, int col) { return world_to_local.data[row * 3 + col]; };
|
||||
|
||||
Common::Vec3 relative_acceleration{};
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const float dt =
|
||||
velocity_history->has_pose_sample
|
||||
? std::chrono::duration_cast<std::chrono::duration<float>>(now -
|
||||
velocity_history->previous_time)
|
||||
.count()
|
||||
: 0.0f;
|
||||
|
||||
Common::Vec3 current_position{};
|
||||
std::optional<Common::Vec3> pose_velocity;
|
||||
if (has_grip_pose)
|
||||
{
|
||||
current_position = ToVec3(controller.grip_pose.position);
|
||||
if (velocity_history->has_pose_sample && dt > 0.001f)
|
||||
pose_velocity = (current_position - velocity_history->previous_position) / dt;
|
||||
}
|
||||
|
||||
std::optional<Common::Vec3> current_velocity;
|
||||
if (controller.grip_velocity.linear_valid)
|
||||
current_velocity = ToVec3(controller.grip_velocity.linear);
|
||||
|
||||
if (pose_velocity)
|
||||
{
|
||||
// Blend runtime velocity with pose-derived velocity to improve push/pull response on
|
||||
// runtimes that heavily smooth linear velocity.
|
||||
if (current_velocity)
|
||||
current_velocity = (*current_velocity + *pose_velocity) * 0.5f;
|
||||
else
|
||||
current_velocity = *pose_velocity;
|
||||
}
|
||||
|
||||
if (current_velocity && velocity_history->has_velocity_sample && dt > 0.001f)
|
||||
{
|
||||
const Common::Vec3 world_accel = (*current_velocity - velocity_history->previous_velocity) / dt;
|
||||
relative_acceleration = world_to_local * world_accel;
|
||||
}
|
||||
|
||||
if (has_grip_pose)
|
||||
{
|
||||
velocity_history->previous_position = current_position;
|
||||
velocity_history->previous_time = now;
|
||||
velocity_history->has_pose_sample = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
velocity_history->has_pose_sample = false;
|
||||
velocity_history->has_velocity_sample = false;
|
||||
}
|
||||
|
||||
if (current_velocity)
|
||||
{
|
||||
velocity_history->previous_velocity = *current_velocity;
|
||||
velocity_history->has_velocity_sample = true;
|
||||
}
|
||||
else if (!velocity_history->has_pose_sample)
|
||||
{
|
||||
velocity_history->has_velocity_sample = false;
|
||||
}
|
||||
|
||||
float gx = -get_matrix(0, 1);
|
||||
float gz = get_matrix(1, 1);
|
||||
float gy = get_matrix(2, 1);
|
||||
|
||||
gx -= relative_acceleration.x / float(MathUtil::GRAVITY_ACCELERATION);
|
||||
gz += relative_acceleration.y / float(MathUtil::GRAVITY_ACCELERATION);
|
||||
gy += relative_acceleration.z / float(MathUtil::GRAVITY_ACCELERATION);
|
||||
|
||||
out.acceleration = Common::Vec3(gx, gy, gz) * float(MathUtil::GRAVITY_ACCELERATION);
|
||||
|
||||
if (controller.grip_velocity.angular_valid)
|
||||
{
|
||||
const Common::Vec3 world_angular_velocity = ToVec3(controller.grip_velocity.angular);
|
||||
const Common::Vec3 local_angular_velocity = world_to_local * world_angular_velocity;
|
||||
out.angular_velocity =
|
||||
Common::Vec3(local_angular_velocity.x, local_angular_velocity.z, local_angular_velocity.y);
|
||||
}
|
||||
|
||||
// Store raw aim orientation and position — IR angles and distance are computed
|
||||
// in ApplyOpenXRIRCenter relative to the captured reference, making the mapping
|
||||
// headset-independent.
|
||||
if (has_aim_pose)
|
||||
{
|
||||
out.aim_orientation = ToQuaternion(controller.aim_pose.orientation).Normalized();
|
||||
out.aim_position = ToVec3(controller.aim_pose.position);
|
||||
out.has_aim = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
out.aim_orientation = reference_orientation;
|
||||
out.has_aim = false;
|
||||
}
|
||||
|
||||
// IR x/y/z left as NaN/0/0 — will be filled by ApplyOpenXRIRCenter
|
||||
return out;
|
||||
}
|
||||
|
||||
std::array<OpenXRIRCenter, MAX_BBMOTES> s_openxr_ir_centers{};
|
||||
|
||||
void ApplyOpenXRIRCenter(unsigned int wiimote_index, OpenXRWiimoteState* state,
|
||||
const Common::VR::OpenXRPoseState& head_pose)
|
||||
{
|
||||
if (wiimote_index >= MAX_BBMOTES || !state->has_aim)
|
||||
return;
|
||||
|
||||
auto& center = s_openxr_ir_centers[wiimote_index];
|
||||
|
||||
// On first valid frame or after recenter, capture the HMD forward direction as reference.
|
||||
// The cursor center corresponds to where the user is looking, and controller aim
|
||||
// deviation from that direction moves the cursor.
|
||||
if (!center.initialized)
|
||||
{
|
||||
if (head_pose.valid)
|
||||
center.reference_orientation = ToQuaternion(head_pose.orientation).Normalized();
|
||||
else
|
||||
center.reference_orientation = state->aim_orientation;
|
||||
center.reference_position = state->aim_position;
|
||||
center.initialized = true;
|
||||
}
|
||||
|
||||
// Compute the relative rotation: how much the controller aim deviates from the
|
||||
// HMD forward direction. conjugate(reference) * current_aim
|
||||
const Common::Quaternion relative =
|
||||
center.reference_orientation.Conjugate() * state->aim_orientation;
|
||||
|
||||
// Transform the forward vector (-Z) by the relative rotation to get the
|
||||
// local-frame pointing direction
|
||||
const Common::Vec3 local_forward = relative * Common::Vec3{0.0f, 0.0f, -1.0f};
|
||||
|
||||
// Extract yaw and pitch from the local forward vector
|
||||
// In OpenXR space: X=right, Y=up, Z=back (right-handed)
|
||||
// local_forward.x = left/right deviation (yaw)
|
||||
// local_forward.y = up/down deviation (pitch)
|
||||
// local_forward.z = forward component (should be near -1 when pointing straight)
|
||||
const float forward_len =
|
||||
std::sqrt(local_forward.x * local_forward.x + local_forward.z * local_forward.z);
|
||||
const float yaw = std::atan2(local_forward.x, -local_forward.z);
|
||||
const float pitch = std::atan2(local_forward.y, std::max(forward_len, 0.001f));
|
||||
|
||||
// Get per-wiimote cursor ranges for normalization
|
||||
const OpenXRCursorRanges ranges = GetOpenXRCursorRanges(wiimote_index);
|
||||
|
||||
// Normalize to [-1, 1] range based on configured total yaw/pitch
|
||||
// Positive yaw = pointing right = positive ir_x (moves cursor right)
|
||||
// Positive pitch = pointing up = positive ir_y (moves cursor up)
|
||||
state->ir_x = yaw / std::max(ranges.yaw_half_range, 0.001f);
|
||||
state->ir_y = pitch / std::max(ranges.pitch_half_range, 0.001f);
|
||||
|
||||
// Compute forward/backward distance (ir_z) from position delta projected onto
|
||||
// the reference aim direction. This changes the spacing between the two virtual
|
||||
// IR sensor bar dots — closer = dots farther apart, farther = dots closer together.
|
||||
// Scale: 0.3m of forward movement = ir_z of -1 (moves 1m closer to sensor bar).
|
||||
// Matches the old Hydra behavior (300mm per unit for Z axis).
|
||||
constexpr float Z_METERS_PER_UNIT = 0.3f;
|
||||
const Common::Vec3 ref_forward =
|
||||
center.reference_orientation * Common::Vec3{0.0f, 0.0f, -1.0f};
|
||||
const Common::Vec3 pos_delta = state->aim_position - center.reference_position;
|
||||
const float forward_displacement = pos_delta.Dot(ref_forward);
|
||||
state->ir_z = std::clamp(-forward_displacement / Z_METERS_PER_UNIT, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
ControllerEmu::InputOverrideFunction CreateOpenXRInputOverrideFunction(unsigned int wiimote_index,
|
||||
bool prefer_left_hand)
|
||||
{
|
||||
OpenXRWiimoteState cached_state;
|
||||
OpenXRVelocityHistory left_velocity_history;
|
||||
OpenXRVelocityHistory right_velocity_history;
|
||||
|
||||
return [wiimote_index, prefer_left_hand, cached_state, left_velocity_history,
|
||||
right_velocity_history](std::string_view group_name, std::string_view control_name,
|
||||
ControlState) mutable -> std::optional<ControlState> {
|
||||
if (s_wiimote_sources[wiimote_index].load() != WiimoteSource::OpenXR)
|
||||
return std::nullopt;
|
||||
|
||||
const Common::VR::OpenXRInputSnapshot snapshot = Common::VR::OpenXRInputState::GetSnapshot();
|
||||
if (!snapshot.runtime_active)
|
||||
return std::nullopt;
|
||||
|
||||
if (cached_state.generation != snapshot.generation)
|
||||
{
|
||||
const auto& left = snapshot.controllers[0];
|
||||
const auto& right = snapshot.controllers[1];
|
||||
|
||||
const auto right_valid = right.grip_pose.valid || right.aim_pose.valid;
|
||||
const auto left_valid = left.grip_pose.valid || left.aim_pose.valid;
|
||||
|
||||
if (!prefer_left_hand && right_valid)
|
||||
{
|
||||
cached_state =
|
||||
BuildOpenXRState(right, &right_velocity_history);
|
||||
ApplyOpenXRIRCenter(wiimote_index, &cached_state, snapshot.head_pose);
|
||||
}
|
||||
else if (left_valid)
|
||||
{
|
||||
cached_state =
|
||||
BuildOpenXRState(left, &left_velocity_history);
|
||||
ApplyOpenXRIRCenter(wiimote_index, &cached_state, snapshot.head_pose);
|
||||
}
|
||||
else
|
||||
{
|
||||
cached_state = {};
|
||||
}
|
||||
|
||||
cached_state.generation = snapshot.generation;
|
||||
}
|
||||
|
||||
if (group_name == WiimoteEmu::Wiimote::ACCELEROMETER_GROUP)
|
||||
{
|
||||
if (control_name == ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE)
|
||||
return cached_state.acceleration.x;
|
||||
if (control_name == ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE)
|
||||
return cached_state.acceleration.y;
|
||||
if (control_name == ControllerEmu::ReshapableInput::Z_INPUT_OVERRIDE)
|
||||
return cached_state.acceleration.z;
|
||||
}
|
||||
else if (group_name == WiimoteEmu::Wiimote::GYROSCOPE_GROUP)
|
||||
{
|
||||
if (control_name == ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE)
|
||||
return cached_state.angular_velocity.x;
|
||||
if (control_name == ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE)
|
||||
return cached_state.angular_velocity.y;
|
||||
if (control_name == ControllerEmu::ReshapableInput::Z_INPUT_OVERRIDE)
|
||||
return cached_state.angular_velocity.z;
|
||||
}
|
||||
else if (group_name == WiimoteEmu::Wiimote::IR_GROUP)
|
||||
{
|
||||
// Handle recenter signal from EmulatePoint — reset the reference orientation
|
||||
// so the current aim direction becomes the new center.
|
||||
if (control_name == "Recenter")
|
||||
{
|
||||
if (wiimote_index < MAX_BBMOTES)
|
||||
s_openxr_ir_centers[wiimote_index].initialized = false;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (control_name == ControllerEmu::ReshapableInput::X_INPUT_OVERRIDE)
|
||||
return std::isnan(cached_state.ir_x) ? 0.0 : cached_state.ir_x;
|
||||
if (control_name == ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE)
|
||||
return static_cast<double>(cached_state.ir_y);
|
||||
if (control_name == ControllerEmu::ReshapableInput::Z_INPUT_OVERRIDE)
|
||||
return static_cast<double>(cached_state.ir_z);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
};
|
||||
}
|
||||
|
||||
std::array<bool, MAX_BBMOTES> s_openxr_overrides_enabled{};
|
||||
|
||||
void UpdateOpenXRInputOverride(unsigned int index, WiimoteSource source)
|
||||
{
|
||||
auto* wiimote =
|
||||
static_cast<WiimoteEmu::Wiimote*>(::Wiimote::GetConfig()->GetController(index));
|
||||
if (!wiimote)
|
||||
return;
|
||||
|
||||
auto apply_to_attachments = [wiimote](const ControllerEmu::InputOverrideFunction& override_func) {
|
||||
auto* attachments_group = static_cast<ControllerEmu::Attachments*>(
|
||||
wiimote->GetWiimoteGroup(WiimoteEmu::WiimoteGroup::Attachments));
|
||||
if (!attachments_group)
|
||||
return;
|
||||
|
||||
for (const auto& attachment : attachments_group->GetAttachmentList())
|
||||
{
|
||||
if (!attachment)
|
||||
continue;
|
||||
|
||||
if (override_func)
|
||||
attachment->SetInputOverrideFunction(override_func);
|
||||
else
|
||||
attachment->ClearInputOverrideFunction();
|
||||
}
|
||||
};
|
||||
|
||||
if (source == WiimoteSource::OpenXR)
|
||||
{
|
||||
s_openxr_ir_centers[index] = {};
|
||||
wiimote->SetInputOverrideFunction(CreateOpenXRInputOverrideFunction(index, false));
|
||||
apply_to_attachments(CreateOpenXRInputOverrideFunction(index, true));
|
||||
s_openxr_overrides_enabled[index] = true;
|
||||
}
|
||||
else if (s_openxr_overrides_enabled[index])
|
||||
{
|
||||
s_openxr_ir_centers[index] = {};
|
||||
wiimote->ClearInputOverrideFunction();
|
||||
apply_to_attachments({});
|
||||
s_openxr_overrides_enabled[index] = false;
|
||||
}
|
||||
}
|
||||
#else
|
||||
void UpdateOpenXRInputOverride(unsigned int, WiimoteSource)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
WiimoteSource GetSource(unsigned int index)
|
||||
{
|
||||
return s_wiimote_sources[index];
|
||||
@@ -44,9 +452,12 @@ void OnSourceChanged(unsigned int index, WiimoteSource source)
|
||||
if (previous_source == source)
|
||||
{
|
||||
// No change. Do nothing.
|
||||
UpdateOpenXRInputOverride(index, source);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateOpenXRInputOverride(index, source);
|
||||
|
||||
WiimoteReal::HandleWiimoteSourceChange(index);
|
||||
|
||||
const Core::CPUThreadGuard guard(Core::System::GetInstance());
|
||||
@@ -79,6 +490,7 @@ HIDWiimote* GetHIDWiimoteSource(unsigned int index)
|
||||
switch (GetSource(index))
|
||||
{
|
||||
case WiimoteSource::Emulated:
|
||||
case WiimoteSource::OpenXR:
|
||||
hid_source = static_cast<WiimoteEmu::Wiimote*>(::Wiimote::GetConfig()->GetController(index));
|
||||
break;
|
||||
|
||||
@@ -104,6 +516,49 @@ InputConfig* GetConfig()
|
||||
return &s_config;
|
||||
}
|
||||
|
||||
std::optional<OpenXRWiiRemoteState> GetOpenXRWiiRemoteState(unsigned int index)
|
||||
{
|
||||
#ifdef ENABLE_VR
|
||||
if (index >= MAX_WIIMOTES || s_wiimote_sources[index].load() != WiimoteSource::OpenXR)
|
||||
return std::nullopt;
|
||||
|
||||
if (const auto right = GetOpenXRHandState(false))
|
||||
return right;
|
||||
return GetOpenXRHandState(true);
|
||||
#else
|
||||
return std::nullopt;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::optional<OpenXRWiiRemoteState> GetOpenXRHandState(bool left_hand)
|
||||
{
|
||||
#ifdef ENABLE_VR
|
||||
const Common::VR::OpenXRInputSnapshot snapshot = Common::VR::OpenXRInputState::GetSnapshot();
|
||||
if (!snapshot.runtime_active)
|
||||
return std::nullopt;
|
||||
|
||||
const auto& controller = snapshot.controllers[left_hand ? 0 : 1];
|
||||
if (!controller.grip_pose.valid && !controller.aim_pose.valid)
|
||||
return std::nullopt;
|
||||
|
||||
OpenXRVelocityHistory throwaway_velocity_history;
|
||||
OpenXRWiimoteState state =
|
||||
BuildOpenXRState(controller, &throwaway_velocity_history);
|
||||
// This path doesn't have a wiimote index, use 0 for reference-based IR
|
||||
ApplyOpenXRIRCenter(0, &state, snapshot.head_pose);
|
||||
|
||||
OpenXRWiiRemoteState out;
|
||||
out.acceleration = {state.acceleration.x, state.acceleration.y, state.acceleration.z};
|
||||
out.angular_velocity = {state.angular_velocity.x, state.angular_velocity.y, state.angular_velocity.z};
|
||||
out.ir_x = state.ir_x;
|
||||
out.ir_y = state.ir_y;
|
||||
out.ir_visible = !std::isnan(state.ir_x);
|
||||
return out;
|
||||
#else
|
||||
return std::nullopt;
|
||||
#endif
|
||||
}
|
||||
|
||||
ControllerEmu::ControlGroup* GetWiimoteGroup(int number, WiimoteEmu::WiimoteGroup group)
|
||||
{
|
||||
return static_cast<WiimoteEmu::Wiimote*>(s_config.GetController(number))->GetWiimoteGroup(group);
|
||||
@@ -233,7 +688,7 @@ void DoState(PointerWrap& p)
|
||||
auto state_wiimote_source = u8(source);
|
||||
p.Do(state_wiimote_source);
|
||||
|
||||
if (WiimoteSource(state_wiimote_source) == WiimoteSource::Emulated)
|
||||
if (IsEmulatedSource(WiimoteSource(state_wiimote_source)))
|
||||
{
|
||||
// Sync complete state of emulated wiimotes.
|
||||
static_cast<WiimoteEmu::Wiimote*>(s_config.GetController(i))->DoState(p);
|
||||
@@ -248,4 +703,13 @@ void DoState(PointerWrap& p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RecenterOpenXRPointer(unsigned int wiimote_index)
|
||||
{
|
||||
#ifdef ENABLE_VR
|
||||
if (wiimote_index < MAX_BBMOTES)
|
||||
s_openxr_ir_centers[wiimote_index].initialized = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
|
||||
#include "Common/Common.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
@@ -49,6 +50,7 @@ enum class WiimoteSource
|
||||
None = 0,
|
||||
Emulated = 1,
|
||||
Real = 2,
|
||||
OpenXR = 3,
|
||||
};
|
||||
|
||||
namespace WiimoteCommon
|
||||
@@ -74,6 +76,15 @@ enum class InitializeMode
|
||||
// The Real Wii Remote sends report every ~5ms (200 Hz).
|
||||
constexpr int UPDATE_FREQ = 200;
|
||||
|
||||
struct OpenXRWiiRemoteState
|
||||
{
|
||||
std::array<float, 3> acceleration{};
|
||||
std::array<float, 3> angular_velocity{};
|
||||
float ir_x = 0.0f;
|
||||
float ir_y = 0.0f;
|
||||
bool ir_visible = false;
|
||||
};
|
||||
|
||||
void Shutdown();
|
||||
void Initialize(InitializeMode init_mode);
|
||||
void ResetAllWiimotes();
|
||||
@@ -81,7 +92,10 @@ void LoadConfig();
|
||||
void GenerateDynamicInputTextures();
|
||||
void Resume();
|
||||
void Pause();
|
||||
std::optional<OpenXRWiiRemoteState> GetOpenXRWiiRemoteState(unsigned int index);
|
||||
std::optional<OpenXRWiiRemoteState> GetOpenXRHandState(bool left_hand);
|
||||
|
||||
void RecenterOpenXRPointer(unsigned int wiimote_index);
|
||||
void DoState(PointerWrap& p);
|
||||
InputConfig* GetConfig();
|
||||
ControllerEmu::ControlGroup* GetWiimoteGroup(int number, WiimoteEmu::WiimoteGroup group);
|
||||
|
||||
@@ -228,8 +228,13 @@ void EmulatePoint(MotionState* state, ControllerEmu::Cursor* ir_group,
|
||||
|
||||
if (!cursor.IsVisible())
|
||||
{
|
||||
const float recenter_x = state->recenter_x;
|
||||
const float recenter_y = state->recenter_y;
|
||||
|
||||
// Move the wiimote a kilometer forward so the sensor bar is always behind it.
|
||||
*state = {};
|
||||
state->recenter_x = recenter_x;
|
||||
state->recenter_y = recenter_y;
|
||||
state->position = {0, -1000, 0};
|
||||
return;
|
||||
}
|
||||
@@ -246,12 +251,55 @@ void EmulatePoint(MotionState* state, ControllerEmu::Cursor* ir_group,
|
||||
const float yaw_scale = ir_group->GetTotalYaw() / 2;
|
||||
const float pitch_scale = ir_group->GetTotalPitch() / 2;
|
||||
|
||||
// Check for Z override (forward/backward distance from VR controllers).
|
||||
// Z override is in [-1, 1] range: -1 = closest (1m), 0 = neutral (2m), +1 = farthest (3m).
|
||||
float distance = NEUTRAL_DISTANCE;
|
||||
if (override_func)
|
||||
{
|
||||
if (const auto z_override = override_func(ir_group->name,
|
||||
ControllerEmu::ReshapableInput::Z_INPUT_OVERRIDE, 0.0))
|
||||
{
|
||||
distance = NEUTRAL_DISTANCE + static_cast<float>(*z_override);
|
||||
distance = std::clamp(distance, 0.5f, 4.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Just jump to the target position.
|
||||
state->position = {0, NEUTRAL_DISTANCE, -height};
|
||||
state->position = {0, distance, -height};
|
||||
state->velocity = {};
|
||||
state->acceleration = {};
|
||||
|
||||
const auto target_angle = Common::Vec3(pitch_scale * -cursor.y, 0, yaw_scale * -cursor.x);
|
||||
float centered_x, centered_y;
|
||||
const bool recenter_pressed = ir_group->controls[5]->GetState<bool>();
|
||||
|
||||
if (override_func)
|
||||
{
|
||||
// When an input override is active (e.g. OpenXR VR controllers), recentering resets
|
||||
// the override's reference orientation via a "Recenter" signal. Skip the offset-based
|
||||
// recenter to avoid shifting the usable cursor range off-screen.
|
||||
if (recenter_pressed && !state->recenter_button_held)
|
||||
override_func(ir_group->name, "Recenter", 0.0);
|
||||
state->recenter_button_held = recenter_pressed;
|
||||
|
||||
centered_x = std::clamp(cursor.x, -1.0, 1.0);
|
||||
centered_y = std::clamp(cursor.y, -1.0, 1.0);
|
||||
state->recenter_x = 0.0f;
|
||||
state->recenter_y = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (recenter_pressed && !state->recenter_button_held)
|
||||
{
|
||||
state->recenter_x = cursor.x;
|
||||
state->recenter_y = cursor.y;
|
||||
}
|
||||
state->recenter_button_held = recenter_pressed;
|
||||
|
||||
centered_x = std::clamp(cursor.x - state->recenter_x, -1.0, 1.0);
|
||||
centered_y = std::clamp(cursor.y - state->recenter_y, -1.0, 1.0);
|
||||
}
|
||||
|
||||
const auto target_angle = Common::Vec3(pitch_scale * -centered_y, 0, yaw_scale * -centered_x);
|
||||
|
||||
// If cursor was hidden, jump to the target angle immediately.
|
||||
if (state->position.y < 0)
|
||||
|
||||
@@ -47,6 +47,11 @@ struct IMUCursorState
|
||||
struct MotionState : PositionalState, RotationalState
|
||||
{
|
||||
MotionState() = default;
|
||||
|
||||
// Cursor center offset used by pointer recenter.
|
||||
float recenter_x = 0.0f;
|
||||
float recenter_y = 0.0f;
|
||||
bool recenter_button_held = false;
|
||||
};
|
||||
|
||||
// Note that 'gyroscope' is rotation of world around device.
|
||||
|
||||
@@ -82,10 +82,12 @@ void Nunchuk::BuildDesiredExtensionState(DesiredExtensionState* target_state)
|
||||
const auto transformation =
|
||||
GetRotationalMatrix(-m_tilt_state.angle) * GetRotationalMatrix(-m_swing_state.angle);
|
||||
|
||||
Common::Vec3 imu_accel =
|
||||
m_imu_accelerometer->GetState().value_or(Common::Vec3(0, 0, float(GRAVITY_ACCELERATION)));
|
||||
|
||||
Common::Vec3 accel =
|
||||
transformation *
|
||||
(m_swing_state.acceleration +
|
||||
m_imu_accelerometer->GetState().value_or(Common::Vec3(0, 0, float(GRAVITY_ACCELERATION))));
|
||||
(m_swing_state.acceleration + imu_accel);
|
||||
|
||||
// shake
|
||||
accel += m_shake_state.acceleration;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "Common/MathUtil.h"
|
||||
|
||||
#include "Core/Config/MainSettings.h"
|
||||
#include "Core/Config/SYSCONFSettings.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/Wiimote.h"
|
||||
|
||||
@@ -509,6 +510,7 @@ void Wiimote::BuildDesiredWiimoteState(DesiredWiimoteState* target_state,
|
||||
GetTotalTransformation(),
|
||||
Common::Vec2(m_fov_x_setting.GetValue(), m_fov_y_setting.GetValue()) / 360 *
|
||||
float(MathUtil::TAU));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -626,13 +628,16 @@ void Wiimote::SendDataReport(const DesiredWiimoteState& target_state)
|
||||
|
||||
u8* ir_data = rpt_builder.GetIRDataPtr();
|
||||
const u8 ir_size = rpt_builder.GetIRDataSize();
|
||||
const int ir_read_size =
|
||||
m_i2c_bus.BusRead(CameraLogic::I2C_ADDR, camera_data_offset, ir_size, ir_data);
|
||||
|
||||
if (ir_size != m_i2c_bus.BusRead(CameraLogic::I2C_ADDR, camera_data_offset, ir_size, ir_data))
|
||||
if (ir_size != ir_read_size)
|
||||
{
|
||||
// This happens when IR reporting is enabled but the camera hardware is disabled.
|
||||
// It commonly occurs when changing IR sensitivity.
|
||||
std::fill_n(ir_data, ir_size, u8(0xff));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Extension port:
|
||||
|
||||
@@ -114,6 +114,36 @@ constexpr std::array<const char*, NUM_HOTKEYS> s_hotkey_labels{{
|
||||
_trans("Toggle Texture Dumping"),
|
||||
_trans("Toggle Custom Textures"),
|
||||
|
||||
_trans("Toggle OpenXR"),
|
||||
_trans("Reset VR Position"),
|
||||
_trans("Decrease Units Per Meter"),
|
||||
_trans("Increase Units Per Meter"),
|
||||
_trans("Decrease Lean Back Angle"),
|
||||
_trans("Increase Lean Back Angle"),
|
||||
_trans("Decrease Camera Forward"),
|
||||
_trans("Increase Camera Forward"),
|
||||
_trans("Toggle Virtual Screen"),
|
||||
_trans("Decrease Screen Distance"),
|
||||
_trans("Increase Screen Distance"),
|
||||
_trans("Decrease Screen Size"),
|
||||
_trans("Increase Screen Size"),
|
||||
_trans("Decrease Screen Curvature"),
|
||||
_trans("Increase Screen Curvature"),
|
||||
_trans("Toggle Don't Clear Screen"),
|
||||
_trans("Toggle Force VBI"),
|
||||
_trans("Toggle Remove Cinematic Bars"),
|
||||
|
||||
_trans("Toggle Shader Hunting"),
|
||||
_trans("Cycle Hunting Option"),
|
||||
_trans("Cycle Shader Type"),
|
||||
_trans("Cycle Override Handling"),
|
||||
_trans("Previous Shader"),
|
||||
_trans("Next Shader"),
|
||||
_trans("Previous Texture Hash"),
|
||||
_trans("Next Texture Hash"),
|
||||
_trans("Toggle Texture Hash Filter"),
|
||||
_trans("Save Shader Override"),
|
||||
|
||||
// i18n: IR stands for internal resolution
|
||||
_trans("Increase IR"),
|
||||
// i18n: IR stands for internal resolution
|
||||
@@ -311,6 +341,8 @@ constexpr std::array<HotkeyGroupInfo, NUM_HOTKEY_GROUPS> s_groups_info = {
|
||||
{_trans("Controller Profile 3"), HK_NEXT_WIIMOTE_PROFILE_3, HK_PREV_GAME_WIIMOTE_PROFILE_3},
|
||||
{_trans("Controller Profile 4"), HK_NEXT_WIIMOTE_PROFILE_4, HK_PREV_GAME_WIIMOTE_PROFILE_4},
|
||||
{_trans("Graphics Toggles"), HK_TOGGLE_CROP, HK_TOGGLE_TEXTURES},
|
||||
{_trans("VR"), HK_VR_TOGGLE_OPENXR, HK_VR_TOGGLE_REMOVE_CINEMATIC_BARS},
|
||||
{_trans("VR Shader"), HK_VR_SHADER_TOGGLE_HUNTING, HK_VR_SHADER_SAVE_OVERRIDE},
|
||||
{_trans("Internal Resolution"), HK_INCREASE_IR, HK_DECREASE_IR},
|
||||
{_trans("Freelook"), HK_FREELOOK_TOGGLE, HK_FREELOOK_TOGGLE},
|
||||
// i18n: Stereoscopic 3D
|
||||
|
||||
@@ -102,6 +102,36 @@ enum Hotkey
|
||||
HK_TOGGLE_DUMPTEXTURES,
|
||||
HK_TOGGLE_TEXTURES,
|
||||
|
||||
HK_VR_TOGGLE_OPENXR,
|
||||
HK_VR_RESET_POSITION,
|
||||
HK_VR_DECREASE_UNITS_PER_METER,
|
||||
HK_VR_INCREASE_UNITS_PER_METER,
|
||||
HK_VR_DECREASE_LEAN_BACK_ANGLE,
|
||||
HK_VR_INCREASE_LEAN_BACK_ANGLE,
|
||||
HK_VR_DECREASE_CAMERA_FORWARD,
|
||||
HK_VR_INCREASE_CAMERA_FORWARD,
|
||||
HK_VR_TOGGLE_VIRTUAL_SCREEN,
|
||||
HK_VR_DECREASE_SCREEN_DISTANCE,
|
||||
HK_VR_INCREASE_SCREEN_DISTANCE,
|
||||
HK_VR_DECREASE_SCREEN_SIZE,
|
||||
HK_VR_INCREASE_SCREEN_SIZE,
|
||||
HK_VR_DECREASE_SCREEN_CURVATURE,
|
||||
HK_VR_INCREASE_SCREEN_CURVATURE,
|
||||
HK_VR_TOGGLE_DONT_CLEAR_SCREEN,
|
||||
HK_VR_TOGGLE_FORCE_VBI,
|
||||
HK_VR_TOGGLE_REMOVE_CINEMATIC_BARS,
|
||||
|
||||
HK_VR_SHADER_TOGGLE_HUNTING,
|
||||
HK_VR_SHADER_CYCLE_HUNTING_OPTION,
|
||||
HK_VR_SHADER_CYCLE_TYPE,
|
||||
HK_VR_SHADER_CYCLE_HANDLING,
|
||||
HK_VR_SHADER_PREV,
|
||||
HK_VR_SHADER_NEXT,
|
||||
HK_VR_SHADER_PREV_TEXTURE_HASH,
|
||||
HK_VR_SHADER_NEXT_TEXTURE_HASH,
|
||||
HK_VR_SHADER_TOGGLE_TEXTURE_HASH,
|
||||
HK_VR_SHADER_SAVE_OVERRIDE,
|
||||
|
||||
HK_INCREASE_IR,
|
||||
HK_DECREASE_IR,
|
||||
|
||||
@@ -205,6 +235,8 @@ enum HotkeyGroup : int
|
||||
HKGP_CONTROLLER_PROFILE_3,
|
||||
HKGP_CONTROLLER_PROFILE_4,
|
||||
HKGP_GRAPHICS_TOGGLES,
|
||||
HKGP_VR,
|
||||
HKGP_VR_SHADER,
|
||||
HKGP_IR,
|
||||
HKGP_FREELOOK,
|
||||
HKGP_3D_TOGGLE,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <locale>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
@@ -87,6 +88,7 @@ struct CompressAndDumpStateArgs
|
||||
{
|
||||
Common::UniqueBuffer<u8> buffer;
|
||||
std::string filename;
|
||||
CompressionType compression_type = CompressionType::LZ4;
|
||||
std::shared_lock<decltype(s_state_saves_in_progress)> task_lock;
|
||||
};
|
||||
|
||||
@@ -121,8 +123,6 @@ static const std::map<u32, std::pair<std::string, std::string>> s_old_versions =
|
||||
{38, {"4.0-4963", "4.0-5267"}}, {39, {"4.0-5279", "4.0-5525"}}, {40, {"4.0-5531", "4.0-5809"}},
|
||||
{41, {"4.0-5811", "4.0-5923"}}, {42, {"4.0-5925", "4.0-5946"}}};
|
||||
|
||||
static constexpr bool s_use_compression = true;
|
||||
|
||||
// Acquired for tasks that will write state save data to the filesystem.
|
||||
// This allows for later waiting on completion of said tasks when necessary.
|
||||
// We want to maintain a proper order of async operations, e.g. Save, Save, GetInfoString.
|
||||
@@ -353,19 +353,25 @@ static void CompressBufferToFile(std::span<const u8> raw_buffer, File::IOFile& f
|
||||
}
|
||||
}
|
||||
|
||||
static void CreateExtendedHeader(StateExtendedHeader& extended_header, size_t uncompressed_size)
|
||||
static CompressionType GetCompressionType(const SaveOptions& options)
|
||||
{
|
||||
return options.compress ? CompressionType::LZ4 : CompressionType::Uncompressed;
|
||||
}
|
||||
|
||||
static void CreateExtendedHeader(StateExtendedHeader& extended_header, size_t uncompressed_size,
|
||||
CompressionType compression_type)
|
||||
{
|
||||
StateExtendedBaseHeader& base_header = extended_header.base_header;
|
||||
base_header.header_version = EXTENDED_HEADER_VERSION;
|
||||
base_header.compression_type =
|
||||
s_use_compression ? CompressionType::LZ4 : CompressionType::Uncompressed;
|
||||
base_header.compression_type = compression_type;
|
||||
base_header.payload_offset = COMPRESSED_DATA_OFFSET;
|
||||
base_header.uncompressed_size = uncompressed_size;
|
||||
|
||||
// If more fields are added to StateExtendedHeader, set them here.
|
||||
}
|
||||
|
||||
static void WriteHeadersToFile(size_t uncompressed_size, File::IOFile& f)
|
||||
static void WriteHeadersToFile(size_t uncompressed_size, CompressionType compression_type,
|
||||
File::IOFile& f)
|
||||
{
|
||||
StateHeader header{};
|
||||
SConfig::GetInstance().GetGameID().copy(header.legacy_header.game_id,
|
||||
@@ -377,7 +383,7 @@ static void WriteHeadersToFile(size_t uncompressed_size, File::IOFile& f)
|
||||
header.version_header.version_string_length = static_cast<u32>(header.version_string.length());
|
||||
|
||||
StateExtendedHeader extended_header{};
|
||||
CreateExtendedHeader(extended_header, uncompressed_size);
|
||||
CreateExtendedHeader(extended_header, uncompressed_size, compression_type);
|
||||
|
||||
f.WriteArray(&header.legacy_header, 1);
|
||||
f.WriteArray(&header.version_header, 1);
|
||||
@@ -411,9 +417,9 @@ static void CompressAndDumpState(Core::System& system, const CompressAndDumpStat
|
||||
return;
|
||||
}
|
||||
|
||||
WriteHeadersToFile(buffer.size(), f);
|
||||
WriteHeadersToFile(buffer.size(), save_args.compression_type, f);
|
||||
|
||||
if (s_use_compression)
|
||||
if (save_args.compression_type == CompressionType::LZ4)
|
||||
CompressBufferToFile(buffer, f);
|
||||
else
|
||||
f.WriteBytes(buffer.data(), buffer.size());
|
||||
@@ -467,7 +473,7 @@ static void CompressAndDumpState(Core::System& system, const CompressAndDumpStat
|
||||
}
|
||||
}
|
||||
|
||||
static void SaveAsFromCore(Core::System& system, std::string filename)
|
||||
static void SaveAsFromCore(Core::System& system, std::string filename, SaveOptions options)
|
||||
{
|
||||
// Try with a buffer a bit larger than the previous state.
|
||||
// This will often avoid the "Measure" step.
|
||||
@@ -482,10 +488,13 @@ static void SaveAsFromCore(Core::System& system, std::string filename)
|
||||
CompressAndDumpStateArgs dump_args{
|
||||
.buffer = std::move(buffer),
|
||||
.filename = std::move(filename),
|
||||
.compression_type = GetCompressionType(options),
|
||||
.task_lock = GetStateSaveTaskLock(),
|
||||
};
|
||||
Core::DisplayMessage("Saving State...", 1000);
|
||||
s_compress_and_dump_thread.EmplaceItem(std::move(dump_args));
|
||||
if (options.wait_for_completion)
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -493,12 +502,28 @@ static void SaveAsFromCore(Core::System& system, std::string filename)
|
||||
}
|
||||
}
|
||||
|
||||
void SaveAs(Core::System& system, std::string filename)
|
||||
void SaveAs(Core::System& system, std::string filename, SaveOptions options)
|
||||
{
|
||||
Core::RunOnCPUThread(
|
||||
system, [&system, filename = std::move(filename), lock = GetStateSaveTaskLock()]() mutable {
|
||||
SaveAsFromCore(system, std::move(filename));
|
||||
});
|
||||
if (options.wait_for_completion)
|
||||
{
|
||||
std::promise<void> completion;
|
||||
auto future = completion.get_future();
|
||||
Core::RunOnCPUThread(system,
|
||||
[&system, filename = std::move(filename), options,
|
||||
lock = GetStateSaveTaskLock(),
|
||||
completion = std::move(completion)]() mutable {
|
||||
SaveAsFromCore(system, std::move(filename), options);
|
||||
completion.set_value();
|
||||
});
|
||||
future.wait();
|
||||
return;
|
||||
}
|
||||
|
||||
Core::RunOnCPUThread(system,
|
||||
[&system, filename = std::move(filename), options,
|
||||
lock = GetStateSaveTaskLock()]() mutable {
|
||||
SaveAsFromCore(system, std::move(filename), options);
|
||||
});
|
||||
}
|
||||
|
||||
static bool GetVersionFromLZO(StateHeader& header, File::IOFile& f)
|
||||
@@ -807,7 +832,7 @@ static void LoadFileStateData(const std::string& filename, Common::UniqueBuffer<
|
||||
static void LoadAsFromCore(Core::System& system, std::string filename)
|
||||
{
|
||||
// Ensure all data has reached the filesystem before trying to use it.
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
WaitForPendingSaves();
|
||||
|
||||
// Save temp buffer for undo load state
|
||||
auto& movie = system.GetMovie();
|
||||
@@ -899,9 +924,9 @@ void Shutdown()
|
||||
s_flush_unsaved_data_hook.reset();
|
||||
}
|
||||
|
||||
void Save(Core::System& system, int slot)
|
||||
void Save(Core::System& system, int slot, SaveOptions options)
|
||||
{
|
||||
SaveAs(system, MakeStateFilename(slot));
|
||||
SaveAs(system, MakeStateFilename(slot), options);
|
||||
}
|
||||
|
||||
void Load(Core::System& system, int slot)
|
||||
@@ -909,6 +934,11 @@ void Load(Core::System& system, int slot)
|
||||
LoadAs(system, MakeStateFilename(slot));
|
||||
}
|
||||
|
||||
void WaitForPendingSaves()
|
||||
{
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
}
|
||||
|
||||
void LoadLastSaved(Core::System& system, int i)
|
||||
{
|
||||
if (!CheckIfStateLoadIsAllowed(system))
|
||||
@@ -916,7 +946,7 @@ void LoadLastSaved(Core::System& system, int i)
|
||||
|
||||
Core::RunOnCPUThread(system, [&system, i] {
|
||||
// Data must reach the filesystem for up to date "UsedSlots".
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
WaitForPendingSaves();
|
||||
|
||||
std::vector<SlotWithTimestamp> used_slots = GetUsedSlotsWithTimestamp();
|
||||
if (std::size_t(i) > used_slots.size())
|
||||
@@ -934,7 +964,7 @@ void SaveFirstSaved(Core::System& system)
|
||||
{
|
||||
Core::RunOnCPUThread(system, [&system, lock = GetStateSaveTaskLock()] {
|
||||
// Data must reach the filesystem for up to date "UsedSlots".
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
WaitForPendingSaves();
|
||||
|
||||
std::vector<SlotWithTimestamp> used_slots = GetUsedSlotsWithTimestamp();
|
||||
auto slot = GetEmptySlot(used_slots);
|
||||
@@ -945,7 +975,7 @@ void SaveFirstSaved(Core::System& system)
|
||||
slot = used_slots.front().slot;
|
||||
}
|
||||
|
||||
SaveAsFromCore(system, MakeStateFilename(*slot));
|
||||
SaveAsFromCore(system, MakeStateFilename(*slot), {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ namespace State
|
||||
// number of states
|
||||
static const u32 NUM_STATES = 10;
|
||||
|
||||
struct SaveOptions
|
||||
{
|
||||
bool compress = true;
|
||||
bool wait_for_completion = false;
|
||||
};
|
||||
|
||||
struct StateHeaderLegacy
|
||||
{
|
||||
char game_id[6];
|
||||
@@ -92,11 +98,12 @@ u64 GetUnixTimeOfSlot(int slot);
|
||||
// These don't happen instantly - they get scheduled as events.
|
||||
// ...But only if we're not in the main CPU thread.
|
||||
// If we're in the main CPU thread then they run immediately instead.
|
||||
void Save(Core::System& system, int slot);
|
||||
void Save(Core::System& system, int slot, SaveOptions options = {});
|
||||
void Load(Core::System& system, int slot);
|
||||
|
||||
void SaveAs(Core::System& system, std::string filename);
|
||||
void SaveAs(Core::System& system, std::string filename, SaveOptions options = {});
|
||||
void LoadAs(Core::System& system, std::string filename);
|
||||
void WaitForPendingSaves();
|
||||
|
||||
void LoadLastSaved(Core::System& system, int i = 1);
|
||||
void SaveFirstSaved(Core::System& system);
|
||||
|
||||
@@ -48,6 +48,16 @@ add_executable(dolphin-emu
|
||||
Config/CheatCodeEditor.h
|
||||
Config/CheatWarningWidget.cpp
|
||||
Config/CheatWarningWidget.h
|
||||
Config/ElementsGroupOverrideAddEditDialog.cpp
|
||||
Config/ElementsGroupOverrideAddEditDialog.h
|
||||
Config/ElementsGroupOverrideWidget.cpp
|
||||
Config/ElementsGroupOverrideWidget.h
|
||||
Config/ElementsHuntingDialog.cpp
|
||||
Config/ElementsHuntingDialog.h
|
||||
Config/ImportShaderFindElementDialog.cpp
|
||||
Config/ImportShaderFindElementDialog.h
|
||||
Config/TextureHashBrowserDialog.cpp
|
||||
Config/TextureHashBrowserDialog.h
|
||||
Config/CommonControllersWidget.cpp
|
||||
Config/CommonControllersWidget.h
|
||||
Config/ConfigControls/ConfigBool.cpp
|
||||
@@ -91,6 +101,8 @@ add_executable(dolphin-emu
|
||||
Config/GameConfigHighlighter.h
|
||||
Config/GameConfigWidget.cpp
|
||||
Config/GameConfigWidget.h
|
||||
Config/VRConfigWidget.cpp
|
||||
Config/VRConfigWidget.h
|
||||
Config/GamecubeControllersWidget.cpp
|
||||
Config/GamecubeControllersWidget.h
|
||||
Config/GeckoCodeWidget.cpp
|
||||
@@ -155,6 +167,8 @@ add_executable(dolphin-emu
|
||||
Config/Mapping/HotkeyTAS.h
|
||||
Config/Mapping/HotkeyUSBEmu.cpp
|
||||
Config/Mapping/HotkeyUSBEmu.h
|
||||
Config/Mapping/HotkeyVR.cpp
|
||||
Config/Mapping/HotkeyVR.h
|
||||
Config/Mapping/HotkeyWii.cpp
|
||||
Config/Mapping/HotkeyWii.h
|
||||
Config/Mapping/IOWindow.cpp
|
||||
@@ -166,6 +180,8 @@ add_executable(dolphin-emu
|
||||
Config/Mapping/MappingIndicator.h
|
||||
Config/Mapping/MappingNumeric.cpp
|
||||
Config/Mapping/MappingNumeric.h
|
||||
Config/Mapping/OpenXRWiimoteConfigSessionController.cpp
|
||||
Config/Mapping/OpenXRWiimoteConfigSessionController.h
|
||||
Config/Mapping/MappingWidget.cpp
|
||||
Config/Mapping/MappingWidget.h
|
||||
Config/Mapping/MappingWindow.cpp
|
||||
@@ -186,6 +202,14 @@ add_executable(dolphin-emu
|
||||
Config/NewPatchDialog.h
|
||||
Config/PatchesWidget.cpp
|
||||
Config/PatchesWidget.h
|
||||
Config/HideObjectAddEditDialog.cpp
|
||||
Config/HideObjectAddEditDialog.h
|
||||
Config/HideObjectWidget.cpp
|
||||
Config/HideObjectWidget.h
|
||||
Config/ShaderOverrideAddEditDialog.cpp
|
||||
Config/ShaderOverrideAddEditDialog.h
|
||||
Config/ShaderOverrideWidget.cpp
|
||||
Config/ShaderOverrideWidget.h
|
||||
Config/PropertiesDialog.cpp
|
||||
Config/PropertiesDialog.h
|
||||
Config/SettingsWindow.cpp
|
||||
@@ -229,6 +253,10 @@ add_executable(dolphin-emu
|
||||
Debugger/CodeViewWidget.h
|
||||
Debugger/CodeWidget.cpp
|
||||
Debugger/CodeWidget.h
|
||||
Debugger/CullingCodeFinderWidget.cpp
|
||||
Debugger/CullingCodeFinderWidget.h
|
||||
Debugger/ShaderHunterWidget.cpp
|
||||
Debugger/ShaderHunterWidget.h
|
||||
Debugger/EditSymbolDialog.cpp
|
||||
Debugger/EditSymbolDialog.h
|
||||
Debugger/GekkoSyntaxHighlight.cpp
|
||||
@@ -382,6 +410,8 @@ add_executable(dolphin-emu
|
||||
Settings/TriforcePane.h
|
||||
Settings/USBDevicePicker.cpp
|
||||
Settings/USBDevicePicker.h
|
||||
Settings/VRPane.cpp
|
||||
Settings/VRPane.h
|
||||
Settings/WiiPane.cpp
|
||||
Settings/WiiPane.h
|
||||
SkylanderPortal/SkylanderModifyDialog.cpp
|
||||
|
||||
@@ -0,0 +1,878 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/ElementsGroupOverrideAddEditDialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSpinBox>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "DolphinQt/Config/TextureHashBrowserDialog.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double UPM_OVERRIDE_MIN = 0.01;
|
||||
constexpr double UPM_OVERRIDE_MAX = 1000.0;
|
||||
constexpr double UPM_OVERRIDE_STEP = 0.01;
|
||||
constexpr int MAX_VISIBLE_TEXTURE_HASH_ROWS = 10;
|
||||
constexpr int TEXTURE_HASH_ROW_HEIGHT = 28;
|
||||
|
||||
QString ToHashHex(u64 hash)
|
||||
{
|
||||
return QStringLiteral("%1").arg(static_cast<qulonglong>(hash), 16, 16, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
QString FormatRuntimeElementSummary(const ShaderHunter::RuntimeElementSignature& sig)
|
||||
{
|
||||
if (!sig.valid)
|
||||
return QObject::tr("No runtime element captured.");
|
||||
|
||||
QStringList parts;
|
||||
parts << QObject::tr("Projection: %1")
|
||||
.arg(sig.perspective ? QObject::tr("Perspective") : QObject::tr("Orthographic"));
|
||||
|
||||
if (sig.perspective)
|
||||
{
|
||||
parts << QObject::tr("FOV: %1 / %2")
|
||||
.arg(sig.perspective_hfov_x100 / 100.0, 0, 'f', 2)
|
||||
.arg(sig.perspective_vfov_x100 / 100.0, 0, 'f', 2);
|
||||
parts << QObject::tr("Near/Far: %1 / %2")
|
||||
.arg(sig.perspective_near_x1000 / 1000.0, 0, 'f', 3)
|
||||
.arg(sig.perspective_far_x100 / 100.0, 0, 'f', 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
parts << QObject::tr("Ortho: L%1 R%2 T%3 B%4")
|
||||
.arg(sig.ortho_left_x100 / 100.0, 0, 'f', 2)
|
||||
.arg(sig.ortho_right_x100 / 100.0, 0, 'f', 2)
|
||||
.arg(sig.ortho_top_x100 / 100.0, 0, 'f', 2)
|
||||
.arg(sig.ortho_bottom_x100 / 100.0, 0, 'f', 2);
|
||||
parts << QObject::tr("Layer: %1").arg(sig.ortho_layer);
|
||||
}
|
||||
|
||||
parts << QObject::tr("Viewport: %1,%2 %3x%4")
|
||||
.arg(sig.viewport_x)
|
||||
.arg(sig.viewport_y)
|
||||
.arg(sig.viewport_width)
|
||||
.arg(sig.viewport_height);
|
||||
parts << QObject::tr("Scissor: %1,%2 %3,%4")
|
||||
.arg(sig.scissor_left)
|
||||
.arg(sig.scissor_top)
|
||||
.arg(sig.scissor_right)
|
||||
.arg(sig.scissor_bottom);
|
||||
parts << QObject::tr("State: alpha=%1 ztest=%2 zupdate=%3 zfunc=%4 blend=%5/%6")
|
||||
.arg(QStringLiteral("0x%1").arg(sig.alpha_test_hex, 8, 16, QLatin1Char('0')))
|
||||
.arg(sig.ztest ? QObject::tr("on") : QObject::tr("off"))
|
||||
.arg(sig.zupdate ? QObject::tr("on") : QObject::tr("off"))
|
||||
.arg(sig.zfunc)
|
||||
.arg(sig.blend_color_update ? QObject::tr("col") : QObject::tr("-"))
|
||||
.arg(sig.blend_alpha_update ? QObject::tr("alpha") : QObject::tr("-"));
|
||||
|
||||
return parts.join(QStringLiteral("\n"));
|
||||
}
|
||||
|
||||
QString FormatTextureSummary(const std::vector<u64>& textures)
|
||||
{
|
||||
if (textures.empty())
|
||||
return QObject::tr("No textures");
|
||||
|
||||
QStringList parts;
|
||||
const int shown = std::min<int>(2, static_cast<int>(textures.size()));
|
||||
for (int i = 0; i < shown; ++i)
|
||||
{
|
||||
parts << QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(textures[static_cast<size_t>(i)]), 16, 16,
|
||||
QLatin1Char('0'));
|
||||
}
|
||||
if (static_cast<int>(textures.size()) > shown)
|
||||
parts << QObject::tr("+%1 more").arg(static_cast<int>(textures.size()) - shown);
|
||||
return parts.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
bool SelectedSubgroupSignaturesEqual(const ElementsGroupManager::SelectedSubgroupSignature& lhs,
|
||||
const ElementsGroupManager::SelectedSubgroupSignature& rhs)
|
||||
{
|
||||
return lhs.vs_family == rhs.vs_family && lhs.ps_family == rhs.ps_family &&
|
||||
lhs.gs_family == rhs.gs_family && lhs.texture_hashes == rhs.texture_hashes;
|
||||
}
|
||||
|
||||
QString FormatSelectedMatchSummary(const ElementsGroupManager::SelectedSubgroupSignature& sig)
|
||||
{
|
||||
QString label = QObject::tr("F %1/%2/%3")
|
||||
.arg(QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(sig.vs_family), 16, 8, QLatin1Char('0')))
|
||||
.arg(QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(sig.ps_family), 16, 8, QLatin1Char('0')))
|
||||
.arg(QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(sig.gs_family), 16, 8, QLatin1Char('0')));
|
||||
label += QObject::tr(" | Tex %1").arg(FormatTextureSummary(sig.texture_hashes));
|
||||
return label;
|
||||
}
|
||||
|
||||
std::vector<TextureHashBrowserEntry> CollectTextureBrowserEntries(
|
||||
const std::vector<ElementsGroupManager::DrawRecord>& draws)
|
||||
{
|
||||
std::vector<TextureHashBrowserEntry> entries;
|
||||
std::unordered_map<u64, size_t> indices;
|
||||
|
||||
for (const auto& draw : draws)
|
||||
{
|
||||
for (size_t i = 0; i < draw.textures.size(); ++i)
|
||||
{
|
||||
const u64 hash = draw.textures[i];
|
||||
if (hash == 0)
|
||||
continue;
|
||||
|
||||
const auto it = indices.find(hash);
|
||||
if (it == indices.end())
|
||||
{
|
||||
indices.emplace(hash, entries.size());
|
||||
entries.push_back(TextureHashBrowserEntry{.hash = hash, .name = draw.texture_names[i]});
|
||||
}
|
||||
else if (entries[it->second].name.empty() && !draw.texture_names[i].empty())
|
||||
{
|
||||
entries[it->second].name = draw.texture_names[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const TextureHashBrowserEntry& a, const TextureHashBrowserEntry& b) {
|
||||
return a.hash < b.hash;
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ElementsGroupOverrideAddEditDialog::ElementsGroupOverrideAddEditDialog(
|
||||
QWidget* parent, const ElementsGroupManager::ElementGroupOverride* edit_override,
|
||||
const std::vector<std::string>& available_flags,
|
||||
const ShaderHunter::ShaderOverride* import_shader,
|
||||
const ElementsGroupManager::RuntimeElementSignature* import_runtime_element)
|
||||
: QDialog(parent), m_available_flags(available_flags)
|
||||
{
|
||||
setWindowTitle(edit_override ? tr("Edit Elements Group Override") :
|
||||
tr("Add Elements Group Override"));
|
||||
setMinimumWidth(420);
|
||||
|
||||
m_name_edit = new QLineEdit;
|
||||
m_name_edit->setPlaceholderText(tr("Override name..."));
|
||||
|
||||
m_comments_edit = new QPlainTextEdit;
|
||||
m_comments_edit->setPlaceholderText(tr("Optional notes..."));
|
||||
m_comments_edit->setTabChangesFocus(true);
|
||||
m_comments_edit->setMinimumHeight(70);
|
||||
|
||||
m_credits_edit = new QLineEdit;
|
||||
m_credits_edit->setPlaceholderText(tr("Optional author/credits..."));
|
||||
|
||||
m_handling_combo = new QComboBox;
|
||||
m_handling_combo->addItem(tr("Skip"),
|
||||
static_cast<int>(ElementsGroupManager::HandlingType::Skip));
|
||||
m_handling_combo->addItem(tr("Screen"),
|
||||
static_cast<int>(ElementsGroupManager::HandlingType::Screen));
|
||||
m_handling_combo->addItem(tr("Fullscreen"),
|
||||
static_cast<int>(ElementsGroupManager::HandlingType::Fullscreen));
|
||||
m_handling_combo->addItem(tr("Fullscreen Mono"),
|
||||
static_cast<int>(ElementsGroupManager::HandlingType::FullscreenMono));
|
||||
m_handling_combo->addItem(tr("Head Locked"),
|
||||
static_cast<int>(ElementsGroupManager::HandlingType::HeadLocked));
|
||||
m_handling_combo->addItem(tr("Flag"),
|
||||
static_cast<int>(ElementsGroupManager::HandlingType::Flag));
|
||||
m_handling_combo->addItem(
|
||||
tr("Units per Meter"),
|
||||
static_cast<int>(ElementsGroupManager::HandlingType::UnitsPerMeter));
|
||||
|
||||
m_layer_label = new QLabel(tr("Layer:"));
|
||||
m_layer_spin = new QSpinBox;
|
||||
m_layer_spin->setRange(-1, 255);
|
||||
m_layer_spin->setSpecialValueText(tr("Default"));
|
||||
m_layer_spin->setValue(-1);
|
||||
|
||||
m_element_depth_label = new QLabel(tr("Element Depth:"));
|
||||
m_element_depth_spin = new QDoubleSpinBox;
|
||||
m_element_depth_spin->setRange(-1.0, 1000.0);
|
||||
m_element_depth_spin->setDecimals(3);
|
||||
m_element_depth_spin->setSingleStep(0.1);
|
||||
m_element_depth_spin->setSpecialValueText(tr("Default"));
|
||||
m_element_depth_spin->setValue(-1.0);
|
||||
|
||||
m_units_per_meter_label = new QLabel(tr("Units per Meter:"));
|
||||
m_units_per_meter_spin = new QDoubleSpinBox;
|
||||
m_units_per_meter_spin->setRange(0.0, UPM_OVERRIDE_MAX);
|
||||
m_units_per_meter_spin->setDecimals(2);
|
||||
m_units_per_meter_spin->setSingleStep(UPM_OVERRIDE_STEP);
|
||||
m_units_per_meter_spin->setSpecialValueText(tr("Default"));
|
||||
m_units_per_meter_spin->setValue(0.0);
|
||||
|
||||
m_flag_label = new QLabel(tr("Flag Group:"));
|
||||
m_flag_edit = new QLineEdit;
|
||||
m_flag_edit->setPlaceholderText(tr("e.g. thermal_visor"));
|
||||
|
||||
m_condition_label = new QLabel(tr("Condition Flag:"));
|
||||
m_condition_combo = new QComboBox;
|
||||
m_condition_combo->setEditable(true);
|
||||
m_condition_combo->addItem(QString());
|
||||
for (const std::string& flag : m_available_flags)
|
||||
m_condition_combo->addItem(QString::fromStdString(flag));
|
||||
|
||||
m_condition_mode_label = new QLabel(tr("Condition Mode:"));
|
||||
m_condition_mode_combo = new QComboBox;
|
||||
m_condition_mode_combo->addItem(tr("Activate"), false);
|
||||
m_condition_mode_combo->addItem(tr("Deactivate"), true);
|
||||
|
||||
m_element_filter_check = new QCheckBox(tr("Use Draw Call Range"));
|
||||
m_element_start_label = new QLabel(tr("Draw Start:"));
|
||||
m_element_start_spin = new QSpinBox;
|
||||
m_element_start_spin->setRange(0, 1000000);
|
||||
m_element_end_label = new QLabel(tr("Draw End:"));
|
||||
m_element_end_spin = new QSpinBox;
|
||||
m_element_end_spin->setRange(0, 1000000);
|
||||
|
||||
m_capture_seed_button = new QPushButton(tr("Use Current Hunt Seed"));
|
||||
m_runtime_element_summary_label = new QLabel;
|
||||
m_runtime_element_summary_label->setWordWrap(true);
|
||||
m_runtime_use_projection_check = new QCheckBox(tr("Projection"));
|
||||
m_runtime_use_layer_check = new QCheckBox(tr("Layer"));
|
||||
m_runtime_use_viewport_check = new QCheckBox(tr("Viewport"));
|
||||
m_runtime_use_scissor_check = new QCheckBox(tr("Scissor"));
|
||||
m_runtime_use_render_state_check = new QCheckBox(tr("Render State"));
|
||||
|
||||
m_texture_mode_combo = new QComboBox;
|
||||
m_texture_mode_combo->addItem(tr("Include"), false);
|
||||
m_texture_mode_combo->addItem(tr("Exclude"), true);
|
||||
m_selected_match_mode_combo = new QComboBox;
|
||||
m_selected_match_mode_combo->addItem(tr("Include"), false);
|
||||
m_selected_match_mode_combo->addItem(tr("Exclude"), true);
|
||||
m_view_textures_button = new QPushButton(tr("View Textures"));
|
||||
m_texture_hash_container = new QWidget;
|
||||
m_texture_hash_layout = new QVBoxLayout(m_texture_hash_container);
|
||||
m_texture_hash_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_texture_hash_layout->setSpacing(4);
|
||||
m_texture_hash_scroll = new QScrollArea;
|
||||
m_texture_hash_scroll->setWidgetResizable(true);
|
||||
m_texture_hash_scroll->setFrameShape(QFrame::NoFrame);
|
||||
m_texture_hash_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_texture_hash_scroll->setWidget(m_texture_hash_container);
|
||||
AddTextureHashField();
|
||||
EnsureTextureHashFieldRows();
|
||||
|
||||
m_selected_match_list = new QListWidget;
|
||||
m_add_current_match_button = new QPushButton(tr("Add Current Hunt Match"));
|
||||
m_remove_selected_match_button = new QPushButton(tr("Remove Selected Match"));
|
||||
|
||||
m_refinement_toggle = new QToolButton;
|
||||
m_refinement_toggle->setText(tr("Advanced Shader Refinement"));
|
||||
m_refinement_toggle->setCheckable(true);
|
||||
m_refinement_toggle->setChecked(false);
|
||||
m_refinement_toggle->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
m_refinement_toggle->setArrowType(Qt::RightArrow);
|
||||
|
||||
m_refinement_frame = new QFrame;
|
||||
m_refinement_frame->setVisible(false);
|
||||
auto* refinement_form = new QFormLayout(m_refinement_frame);
|
||||
m_refinement_enable_check = new QCheckBox(tr("Enable shader refinement"));
|
||||
refinement_form->addRow(m_refinement_enable_check);
|
||||
|
||||
m_refinement_type_combo = new QComboBox;
|
||||
m_refinement_type_combo->addItem(tr("Pixel Shader"),
|
||||
static_cast<int>(ElementsGroupManager::ShaderType::Pixel));
|
||||
m_refinement_type_combo->addItem(tr("Vertex Shader"),
|
||||
static_cast<int>(ElementsGroupManager::ShaderType::Vertex));
|
||||
m_refinement_type_combo->addItem(tr("Geometry Shader"),
|
||||
static_cast<int>(ElementsGroupManager::ShaderType::Geometry));
|
||||
refinement_form->addRow(tr("Shader Type:"), m_refinement_type_combo);
|
||||
|
||||
m_refinement_family_check = new QCheckBox(tr("Use Family Signature"));
|
||||
refinement_form->addRow(m_refinement_family_check);
|
||||
|
||||
m_refinement_hash_label = new QLabel(tr("Exact Hash:"));
|
||||
m_refinement_hash_edit = new QLineEdit;
|
||||
m_refinement_hash_edit->setPlaceholderText(tr("0000000000000000"));
|
||||
refinement_form->addRow(m_refinement_hash_label, m_refinement_hash_edit);
|
||||
|
||||
m_refinement_family_signature_label = new QLabel(tr("Family Signature:"));
|
||||
m_refinement_family_signature_edit = new QLineEdit;
|
||||
m_refinement_family_signature_edit->setPlaceholderText(tr("0000000000000000"));
|
||||
refinement_form->addRow(m_refinement_family_signature_label, m_refinement_family_signature_edit);
|
||||
|
||||
auto* form = new QFormLayout;
|
||||
form->addRow(tr("Name:"), m_name_edit);
|
||||
form->addRow(tr("Handling:"), m_handling_combo);
|
||||
form->addRow(m_layer_label, m_layer_spin);
|
||||
form->addRow(m_element_depth_label, m_element_depth_spin);
|
||||
form->addRow(m_units_per_meter_label, m_units_per_meter_spin);
|
||||
form->addRow(m_flag_label, m_flag_edit);
|
||||
form->addRow(m_condition_label, m_condition_combo);
|
||||
form->addRow(m_condition_mode_label, m_condition_mode_combo);
|
||||
form->addRow(m_element_filter_check);
|
||||
form->addRow(m_element_start_label, m_element_start_spin);
|
||||
form->addRow(m_element_end_label, m_element_end_spin);
|
||||
form->addRow(QString(), m_view_textures_button);
|
||||
form->addRow(tr("Texture Mode:"), m_texture_mode_combo);
|
||||
form->addRow(tr("Texture Filters:"), m_texture_hash_scroll);
|
||||
form->addRow(tr("Selected Match Mode:"), m_selected_match_mode_combo);
|
||||
form->addRow(tr("Selected Matches:"), m_selected_match_list);
|
||||
|
||||
auto* runtime_group_row = new QHBoxLayout;
|
||||
runtime_group_row->addWidget(m_runtime_use_projection_check);
|
||||
runtime_group_row->addWidget(m_runtime_use_layer_check);
|
||||
runtime_group_row->addWidget(m_runtime_use_viewport_check);
|
||||
runtime_group_row->addWidget(m_runtime_use_scissor_check);
|
||||
runtime_group_row->addWidget(m_runtime_use_render_state_check);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->addLayout(form);
|
||||
layout->addWidget(m_capture_seed_button);
|
||||
layout->addWidget(m_runtime_element_summary_label);
|
||||
layout->addLayout(runtime_group_row);
|
||||
auto* selected_match_buttons = new QHBoxLayout;
|
||||
selected_match_buttons->addWidget(m_add_current_match_button);
|
||||
selected_match_buttons->addWidget(m_remove_selected_match_button);
|
||||
layout->addLayout(selected_match_buttons);
|
||||
layout->addWidget(m_refinement_toggle);
|
||||
layout->addWidget(m_refinement_frame);
|
||||
auto* notes_form = new QFormLayout;
|
||||
notes_form->addRow(tr("Comments:"), m_comments_edit);
|
||||
notes_form->addRow(tr("Credits:"), m_credits_edit);
|
||||
layout->addLayout(notes_form);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this,
|
||||
&ElementsGroupOverrideAddEditDialog::OnAccept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(m_handling_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&ElementsGroupOverrideAddEditDialog::RefreshHandlingUi);
|
||||
connect(m_element_filter_check, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
m_element_start_label->setVisible(checked);
|
||||
m_element_start_spin->setVisible(checked);
|
||||
m_element_end_label->setVisible(checked);
|
||||
m_element_end_spin->setVisible(checked);
|
||||
});
|
||||
connect(m_capture_seed_button, &QPushButton::clicked, this,
|
||||
&ElementsGroupOverrideAddEditDialog::CaptureCurrentSeed);
|
||||
connect(m_view_textures_button, &QPushButton::clicked, this,
|
||||
&ElementsGroupOverrideAddEditDialog::ShowTextureBrowser);
|
||||
connect(m_add_current_match_button, &QPushButton::clicked, this,
|
||||
&ElementsGroupOverrideAddEditDialog::AddCurrentHuntMatch);
|
||||
connect(m_remove_selected_match_button, &QPushButton::clicked, this,
|
||||
&ElementsGroupOverrideAddEditDialog::RemoveSelectedMatchFilter);
|
||||
connect(m_selected_match_list, &QListWidget::itemSelectionChanged, this, [this]() {
|
||||
m_remove_selected_match_button->setEnabled(!m_selected_match_list->selectedItems().empty());
|
||||
});
|
||||
for (QCheckBox* checkbox : {m_runtime_use_projection_check, m_runtime_use_layer_check,
|
||||
m_runtime_use_viewport_check, m_runtime_use_scissor_check,
|
||||
m_runtime_use_render_state_check})
|
||||
{
|
||||
connect(checkbox, &QCheckBox::toggled, this, [this]() {
|
||||
m_runtime_element.use_projection = m_runtime_use_projection_check->isChecked();
|
||||
m_runtime_element.use_layer = m_runtime_use_layer_check->isChecked();
|
||||
m_runtime_element.use_viewport = m_runtime_use_viewport_check->isChecked();
|
||||
m_runtime_element.use_scissor = m_runtime_use_scissor_check->isChecked();
|
||||
m_runtime_element.use_render_state = m_runtime_use_render_state_check->isChecked();
|
||||
RefreshRuntimeElementSummary();
|
||||
});
|
||||
}
|
||||
connect(m_refinement_toggle, &QToolButton::toggled, this, [this](bool checked) {
|
||||
m_refinement_widgets_visible = checked;
|
||||
m_refinement_toggle->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow);
|
||||
m_refinement_frame->setVisible(checked);
|
||||
});
|
||||
connect(m_refinement_enable_check, &QCheckBox::toggled, this,
|
||||
&ElementsGroupOverrideAddEditDialog::RefreshRefinementUi);
|
||||
connect(m_refinement_family_check, &QCheckBox::toggled, this,
|
||||
&ElementsGroupOverrideAddEditDialog::RefreshRefinementUi);
|
||||
|
||||
if (edit_override)
|
||||
{
|
||||
m_name_edit->setText(QString::fromStdString(edit_override->name));
|
||||
m_comments_edit->setPlainText(QString::fromStdString(edit_override->comments));
|
||||
m_credits_edit->setText(QString::fromStdString(edit_override->credits));
|
||||
{
|
||||
const int handling_idx = m_handling_combo->findData(static_cast<int>(edit_override->handling));
|
||||
if (handling_idx >= 0)
|
||||
m_handling_combo->setCurrentIndex(handling_idx);
|
||||
}
|
||||
m_layer_spin->setValue(edit_override->layer);
|
||||
m_element_depth_spin->setValue(edit_override->element_depth);
|
||||
if (edit_override->units_per_meter > 0.0f)
|
||||
m_units_per_meter_spin->setValue(edit_override->units_per_meter);
|
||||
m_flag_edit->setText(QString::fromStdString(edit_override->flag_group));
|
||||
{
|
||||
const QString condition = QString::fromStdString(edit_override->condition_flag);
|
||||
int idx = m_condition_combo->findText(condition);
|
||||
if (idx < 0 && !condition.isEmpty())
|
||||
{
|
||||
m_condition_combo->addItem(condition);
|
||||
idx = m_condition_combo->findText(condition);
|
||||
}
|
||||
m_condition_combo->setCurrentIndex(std::max(idx, 0));
|
||||
}
|
||||
{
|
||||
const int idx = m_condition_mode_combo->findData(edit_override->condition_inverted);
|
||||
if (idx >= 0)
|
||||
m_condition_mode_combo->setCurrentIndex(idx);
|
||||
}
|
||||
m_element_filter_check->setChecked(edit_override->element_start >= 0 &&
|
||||
edit_override->element_end >= 0);
|
||||
if (edit_override->element_start >= 0)
|
||||
m_element_start_spin->setValue(edit_override->element_start);
|
||||
if (edit_override->element_end >= 0)
|
||||
m_element_end_spin->setValue(edit_override->element_end);
|
||||
m_edit_element_reference_total = edit_override->element_reference_total;
|
||||
{
|
||||
const int idx = m_texture_mode_combo->findData(edit_override->texture_hashes_excluded);
|
||||
if (idx >= 0)
|
||||
m_texture_mode_combo->setCurrentIndex(idx);
|
||||
}
|
||||
SetTextureHashValues(edit_override->texture_hashes);
|
||||
m_runtime_element = edit_override->runtime_element;
|
||||
m_selected_match_filters = edit_override->selected_match_filter;
|
||||
m_selected_match_filters_excluded = edit_override->selected_match_filter_excluded;
|
||||
m_refinement_enable_check->setChecked(edit_override->refinement_enabled);
|
||||
{
|
||||
const int idx =
|
||||
m_refinement_type_combo->findData(static_cast<int>(edit_override->refinement_type));
|
||||
if (idx >= 0)
|
||||
m_refinement_type_combo->setCurrentIndex(idx);
|
||||
}
|
||||
m_refinement_family_check->setChecked(edit_override->refinement_family_match);
|
||||
m_refinement_hash_edit->setText(ToHashHex(edit_override->refinement_hash));
|
||||
m_refinement_family_signature_edit->setText(
|
||||
ToHashHex(edit_override->refinement_family_signature));
|
||||
}
|
||||
else if (import_shader)
|
||||
{
|
||||
const QString import_name = QString::fromStdString(import_shader->name).trimmed();
|
||||
m_name_edit->setText(import_name.isEmpty() ? tr("Imported Element Override") :
|
||||
tr("Imported from %1").arg(import_name));
|
||||
|
||||
{
|
||||
const int handling_idx =
|
||||
m_handling_combo->findData(static_cast<int>(import_shader->handling));
|
||||
if (handling_idx >= 0)
|
||||
m_handling_combo->setCurrentIndex(handling_idx);
|
||||
}
|
||||
|
||||
{
|
||||
const int idx = m_texture_mode_combo->findData(import_shader->texture_hashes_excluded);
|
||||
if (idx >= 0)
|
||||
m_texture_mode_combo->setCurrentIndex(idx);
|
||||
}
|
||||
SetTextureHashValues(import_shader->texture_hashes);
|
||||
|
||||
m_refinement_enable_check->setChecked(true);
|
||||
{
|
||||
const int idx =
|
||||
m_refinement_type_combo->findData(static_cast<int>(import_shader->type));
|
||||
if (idx >= 0)
|
||||
m_refinement_type_combo->setCurrentIndex(idx);
|
||||
}
|
||||
m_refinement_family_check->setChecked(import_shader->match_mode ==
|
||||
ShaderHunter::MatchMode::ShaderFamily &&
|
||||
import_shader->hash_family_match);
|
||||
m_refinement_hash_edit->setText(ToHashHex(import_shader->hash));
|
||||
m_refinement_family_signature_edit->setText(ToHashHex(import_shader->family_signature));
|
||||
|
||||
if (m_refinement_enable_check->isChecked())
|
||||
m_refinement_toggle->setChecked(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_name_edit->setText(tr("Element Override"));
|
||||
}
|
||||
|
||||
for (const auto& filter : m_selected_match_filters)
|
||||
m_selected_match_list->addItem(FormatSelectedMatchSummary(filter));
|
||||
{
|
||||
const int idx = m_selected_match_mode_combo->findData(m_selected_match_filters_excluded);
|
||||
if (idx >= 0)
|
||||
m_selected_match_mode_combo->setCurrentIndex(idx);
|
||||
}
|
||||
m_remove_selected_match_button->setEnabled(false);
|
||||
|
||||
RefreshHandlingUi();
|
||||
m_element_start_label->setVisible(m_element_filter_check->isChecked());
|
||||
m_element_start_spin->setVisible(m_element_filter_check->isChecked());
|
||||
m_element_end_label->setVisible(m_element_filter_check->isChecked());
|
||||
m_element_end_spin->setVisible(m_element_filter_check->isChecked());
|
||||
RefreshRuntimeElementSummary();
|
||||
RefreshRefinementUi();
|
||||
|
||||
if (import_runtime_element != nullptr && import_runtime_element->valid)
|
||||
{
|
||||
m_runtime_element = *import_runtime_element;
|
||||
RefreshRuntimeElementSummary();
|
||||
}
|
||||
}
|
||||
|
||||
ElementsGroupManager::ElementGroupOverride ElementsGroupOverrideAddEditDialog::GetResult() const
|
||||
{
|
||||
ElementsGroupManager::ElementGroupOverride result;
|
||||
result.name = m_name_edit->text().trimmed().toStdString();
|
||||
result.comments = m_comments_edit->toPlainText().trimmed().toStdString();
|
||||
result.credits = m_credits_edit->text().trimmed().toStdString();
|
||||
result.handling = static_cast<ElementsGroupManager::HandlingType>(
|
||||
m_handling_combo->currentData().toInt());
|
||||
result.runtime_element = m_runtime_element;
|
||||
result.layer = m_layer_spin->value();
|
||||
result.element_depth = m_element_depth_spin->value();
|
||||
result.units_per_meter = m_units_per_meter_spin->value() > 0.0 ? m_units_per_meter_spin->value() :
|
||||
-1.0f;
|
||||
result.texture_hashes = CollectTextureHashValues();
|
||||
result.texture_hashes_excluded =
|
||||
!result.texture_hashes.empty() && m_texture_mode_combo->currentData().toBool();
|
||||
result.selected_match_filter = m_selected_match_filters;
|
||||
result.selected_match_filter_excluded =
|
||||
!result.selected_match_filter.empty() && m_selected_match_mode_combo->currentData().toBool();
|
||||
result.flag_group = m_flag_edit->text().trimmed().toStdString();
|
||||
result.condition_flag = m_condition_combo->currentText().trimmed().toStdString();
|
||||
result.condition_inverted =
|
||||
!result.condition_flag.empty() && m_condition_mode_combo->currentData().toBool();
|
||||
if (m_element_filter_check->isChecked())
|
||||
{
|
||||
result.element_start = m_element_start_spin->value();
|
||||
result.element_end = m_element_end_spin->value();
|
||||
result.element_reference_total = std::max(m_edit_element_reference_total, result.element_end + 1);
|
||||
}
|
||||
result.refinement_enabled = m_refinement_enable_check->isChecked();
|
||||
result.refinement_type = static_cast<ElementsGroupManager::ShaderType>(
|
||||
m_refinement_type_combo->currentData().toInt());
|
||||
result.refinement_family_match = m_refinement_family_check->isChecked();
|
||||
result.refinement_hash = std::strtoull(m_refinement_hash_edit->text().trimmed().toStdString().c_str(),
|
||||
nullptr, 16);
|
||||
result.refinement_family_signature = std::strtoull(
|
||||
m_refinement_family_signature_edit->text().trimmed().toStdString().c_str(), nullptr, 16);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<u64> ElementsGroupOverrideAddEditDialog::CollectTextureHashValues() const
|
||||
{
|
||||
std::vector<u64> hashes;
|
||||
hashes.reserve(m_texture_hash_edits.size());
|
||||
for (QLineEdit* edit : m_texture_hash_edits)
|
||||
{
|
||||
const QString text = edit->text().trimmed();
|
||||
if (text.isEmpty())
|
||||
continue;
|
||||
const u64 parsed = std::strtoull(text.toStdString().c_str(), nullptr, 16);
|
||||
if (parsed != 0)
|
||||
hashes.push_back(parsed);
|
||||
}
|
||||
std::sort(hashes.begin(), hashes.end());
|
||||
hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end());
|
||||
return hashes;
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::SetTextureHashValues(const std::vector<u64>& hashes)
|
||||
{
|
||||
m_updating_texture_hash_fields = true;
|
||||
while (m_texture_hash_edits.size() < hashes.size())
|
||||
AddTextureHashField();
|
||||
for (size_t i = 0; i < hashes.size(); ++i)
|
||||
m_texture_hash_edits[i]->setText(ToHashHex(hashes[i]));
|
||||
for (size_t i = hashes.size(); i < m_texture_hash_edits.size(); ++i)
|
||||
m_texture_hash_edits[i]->clear();
|
||||
m_updating_texture_hash_fields = false;
|
||||
EnsureTextureHashFieldRows();
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::AddTextureHashField(const QString& text)
|
||||
{
|
||||
auto* edit = new QLineEdit;
|
||||
edit->setPlaceholderText(tr("0000000000000000"));
|
||||
edit->setText(text);
|
||||
connect(edit, &QLineEdit::editingFinished, this,
|
||||
[this, edit]() { SanitizeTextureHashField(edit); });
|
||||
connect(edit, &QLineEdit::textChanged, this,
|
||||
[this]() { EnsureTextureHashFieldRows(); });
|
||||
m_texture_hash_layout->addWidget(edit);
|
||||
m_texture_hash_edits.push_back(edit);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::EnsureTextureHashFieldRows()
|
||||
{
|
||||
if (m_updating_texture_hash_fields)
|
||||
return;
|
||||
|
||||
m_updating_texture_hash_fields = true;
|
||||
int filled = 0;
|
||||
for (QLineEdit* edit : m_texture_hash_edits)
|
||||
{
|
||||
if (!edit->text().trimmed().isEmpty())
|
||||
++filled;
|
||||
}
|
||||
|
||||
const int desired_rows = std::max(1, filled + 1);
|
||||
while (static_cast<int>(m_texture_hash_edits.size()) < desired_rows)
|
||||
AddTextureHashField();
|
||||
while (static_cast<int>(m_texture_hash_edits.size()) > desired_rows)
|
||||
{
|
||||
QLineEdit* edit = m_texture_hash_edits.back();
|
||||
if (!edit->text().trimmed().isEmpty())
|
||||
break;
|
||||
m_texture_hash_edits.pop_back();
|
||||
m_texture_hash_layout->removeWidget(edit);
|
||||
delete edit;
|
||||
}
|
||||
const int visible_rows = std::min<int>(desired_rows, MAX_VISIBLE_TEXTURE_HASH_ROWS);
|
||||
const int viewport_height = visible_rows * TEXTURE_HASH_ROW_HEIGHT;
|
||||
m_texture_hash_scroll->setMinimumHeight(viewport_height);
|
||||
m_texture_hash_scroll->setMaximumHeight(viewport_height);
|
||||
m_updating_texture_hash_fields = false;
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::SanitizeTextureHashField(QLineEdit* edit)
|
||||
{
|
||||
if (edit == nullptr || m_updating_texture_hash_fields)
|
||||
return;
|
||||
|
||||
QString text = edit->text().trimmed().toLower();
|
||||
QString filtered;
|
||||
filtered.reserve(text.size());
|
||||
for (const QChar c : text)
|
||||
{
|
||||
if (c.isDigit() || (c >= QLatin1Char('a') && c <= QLatin1Char('f')))
|
||||
filtered.append(c);
|
||||
}
|
||||
if (filtered.size() > 16)
|
||||
filtered = filtered.right(16);
|
||||
edit->setText(filtered);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::RefreshRuntimeElementSummary()
|
||||
{
|
||||
QString summary = FormatRuntimeElementSummary(m_runtime_element);
|
||||
QStringList groups;
|
||||
if (m_runtime_element.use_projection)
|
||||
groups << tr("Projection");
|
||||
if (m_runtime_element.use_layer)
|
||||
groups << tr("Layer");
|
||||
if (m_runtime_element.use_viewport)
|
||||
groups << tr("Viewport");
|
||||
if (m_runtime_element.use_scissor)
|
||||
groups << tr("Scissor");
|
||||
if (m_runtime_element.use_render_state)
|
||||
groups << tr("Render State");
|
||||
|
||||
summary += tr("\nActive Groups: %1")
|
||||
.arg(groups.isEmpty() ? tr("(none)") : groups.join(QStringLiteral(", ")));
|
||||
m_runtime_element_summary_label->setText(summary);
|
||||
|
||||
const QSignalBlocker projection_blocker(m_runtime_use_projection_check);
|
||||
const QSignalBlocker layer_blocker(m_runtime_use_layer_check);
|
||||
const QSignalBlocker viewport_blocker(m_runtime_use_viewport_check);
|
||||
const QSignalBlocker scissor_blocker(m_runtime_use_scissor_check);
|
||||
const QSignalBlocker render_blocker(m_runtime_use_render_state_check);
|
||||
m_runtime_use_projection_check->setChecked(m_runtime_element.use_projection);
|
||||
m_runtime_use_layer_check->setChecked(m_runtime_element.use_layer);
|
||||
m_runtime_use_viewport_check->setChecked(m_runtime_element.use_viewport);
|
||||
m_runtime_use_scissor_check->setChecked(m_runtime_element.use_scissor);
|
||||
m_runtime_use_render_state_check->setChecked(m_runtime_element.use_render_state);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::RefreshHandlingUi()
|
||||
{
|
||||
const auto handling = static_cast<ElementsGroupManager::HandlingType>(
|
||||
m_handling_combo->currentData().toInt());
|
||||
const bool show_layer = (handling == ElementsGroupManager::HandlingType::Screen ||
|
||||
handling == ElementsGroupManager::HandlingType::HeadLocked);
|
||||
const bool show_units_per_meter =
|
||||
(handling == ElementsGroupManager::HandlingType::UnitsPerMeter);
|
||||
const bool is_flag = (handling == ElementsGroupManager::HandlingType::Flag);
|
||||
|
||||
m_layer_label->setVisible(show_layer);
|
||||
m_layer_spin->setVisible(show_layer);
|
||||
m_element_depth_label->setVisible(show_layer);
|
||||
m_element_depth_spin->setVisible(show_layer);
|
||||
m_units_per_meter_label->setVisible(show_units_per_meter);
|
||||
m_units_per_meter_spin->setVisible(show_units_per_meter);
|
||||
m_condition_label->setVisible(!is_flag);
|
||||
m_condition_combo->setVisible(!is_flag);
|
||||
m_condition_mode_label->setVisible(!is_flag);
|
||||
m_condition_mode_combo->setVisible(!is_flag);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::RefreshRefinementUi()
|
||||
{
|
||||
const bool enabled = m_refinement_enable_check->isChecked();
|
||||
const bool use_family = enabled && m_refinement_family_check->isChecked();
|
||||
m_refinement_type_combo->setEnabled(enabled);
|
||||
m_refinement_family_check->setEnabled(enabled);
|
||||
m_refinement_hash_label->setVisible(enabled && !use_family);
|
||||
m_refinement_hash_edit->setVisible(enabled && !use_family);
|
||||
m_refinement_family_signature_label->setVisible(enabled && use_family);
|
||||
m_refinement_family_signature_edit->setVisible(enabled && use_family);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::CaptureCurrentSeed()
|
||||
{
|
||||
const auto status = ElementsGroupManager::GetInstance().GetStatus();
|
||||
if (!status.seed_valid)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("No active Elements Hunting seed is available."));
|
||||
return;
|
||||
}
|
||||
|
||||
m_runtime_element = status.seed_signature;
|
||||
RefreshRuntimeElementSummary();
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::ShowTextureBrowser()
|
||||
{
|
||||
const auto status = ElementsGroupManager::GetInstance().GetStatus();
|
||||
if (!status.seed_valid)
|
||||
{
|
||||
QMessageBox::warning(this, tr("View Textures"),
|
||||
tr("No active Elements Hunting preview scope is available.\n"
|
||||
"Open Elements Hunting and select a seed first."));
|
||||
return;
|
||||
}
|
||||
|
||||
TextureHashBrowserConfig browser_config;
|
||||
browser_config.title = tr("Textures for Current Element Selection");
|
||||
browser_config.empty_info_text =
|
||||
tr("No textures were captured yet for the current preview scope.");
|
||||
browser_config.current_label = tr("current preview scope");
|
||||
browser_config.fetch_current_label = []() {
|
||||
const auto current_status = ElementsGroupManager::GetInstance().GetStatus();
|
||||
return current_status.selected_match_filter_count > 0 ?
|
||||
(current_status.selected_match_filter_excluded ? QObject::tr("unchecked matches only") :
|
||||
QObject::tr("checked matches only")) :
|
||||
QObject::tr("whole current group");
|
||||
};
|
||||
browser_config.initial_selected_hashes = CollectTextureHashValues();
|
||||
browser_config.fetch_current_entries = []() {
|
||||
return CollectTextureBrowserEntries(
|
||||
ElementsGroupManager::GetInstance().GetCurrentTextureSourceDraws());
|
||||
};
|
||||
browser_config.apply_selected_hashes = [this](const std::vector<u64>& hashes) {
|
||||
SetTextureHashValues(hashes);
|
||||
};
|
||||
|
||||
ShowTextureHashBrowserDialog(this, browser_config);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::AddCurrentHuntMatch()
|
||||
{
|
||||
const auto status = ElementsGroupManager::GetInstance().GetStatus();
|
||||
if (!status.highlighted_draw)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("No active Elements Hunting match is available."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto matches = ElementsGroupManager::GetInstance().GetCurrentMatches();
|
||||
if (status.selected_match < 0 || status.selected_match >= static_cast<int>(matches.size()))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("No stable Elements Hunting match is available."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto filter = matches[static_cast<size_t>(status.selected_match)].subgroup;
|
||||
const auto it = std::find_if(m_selected_match_filters.begin(), m_selected_match_filters.end(),
|
||||
[&filter](const ElementsGroupManager::SelectedSubgroupSignature& existing) {
|
||||
return SelectedSubgroupSignaturesEqual(existing, filter);
|
||||
});
|
||||
if (it != m_selected_match_filters.end())
|
||||
return;
|
||||
|
||||
m_selected_match_filters.push_back(filter);
|
||||
m_selected_match_list->addItem(FormatSelectedMatchSummary(filter));
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::RemoveSelectedMatchFilter()
|
||||
{
|
||||
const auto items = m_selected_match_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
|
||||
const int row = m_selected_match_list->row(items[0]);
|
||||
if (row < 0 || row >= static_cast<int>(m_selected_match_filters.size()))
|
||||
return;
|
||||
|
||||
m_selected_match_filters.erase(m_selected_match_filters.begin() + row);
|
||||
delete m_selected_match_list->takeItem(row);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideAddEditDialog::OnAccept()
|
||||
{
|
||||
const auto result = GetResult();
|
||||
if (result.name.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"), tr("Name cannot be empty."));
|
||||
return;
|
||||
}
|
||||
if (!result.runtime_element.valid)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("A runtime element signature is required."));
|
||||
return;
|
||||
}
|
||||
if (!result.runtime_element.use_projection && !result.runtime_element.use_layer &&
|
||||
!result.runtime_element.use_viewport && !result.runtime_element.use_scissor &&
|
||||
!result.runtime_element.use_render_state)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("Enable at least one runtime signature group."));
|
||||
return;
|
||||
}
|
||||
if (result.handling == ElementsGroupManager::HandlingType::Flag && result.flag_group.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("Flag handling requires a flag group name."));
|
||||
return;
|
||||
}
|
||||
if (m_element_filter_check->isChecked() && m_element_end_spin->value() < m_element_start_spin->value())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("Draw End must be greater than or equal to Draw Start."));
|
||||
return;
|
||||
}
|
||||
if (result.refinement_enabled)
|
||||
{
|
||||
if (result.refinement_family_match)
|
||||
{
|
||||
if (result.refinement_family_signature == 0)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("Family refinement requires a non-zero family signature."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (result.refinement_hash == 0)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Group Override"),
|
||||
tr("Exact refinement requires a non-zero shader hash."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "VideoCommon/ElementsGroupManager.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QFrame;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPlainTextEdit;
|
||||
class QPushButton;
|
||||
class QListWidget;
|
||||
class QScrollArea;
|
||||
class QSpinBox;
|
||||
class QToolButton;
|
||||
class QVBoxLayout;
|
||||
class QWidget;
|
||||
|
||||
class ElementsGroupOverrideAddEditDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ElementsGroupOverrideAddEditDialog(
|
||||
QWidget* parent,
|
||||
const ElementsGroupManager::ElementGroupOverride* edit_override = nullptr,
|
||||
const std::vector<std::string>& available_flags = {},
|
||||
const ShaderHunter::ShaderOverride* import_shader = nullptr,
|
||||
const ElementsGroupManager::RuntimeElementSignature* import_runtime_element = nullptr);
|
||||
|
||||
ElementsGroupManager::ElementGroupOverride GetResult() const;
|
||||
|
||||
private:
|
||||
std::vector<u64> CollectTextureHashValues() const;
|
||||
void SetTextureHashValues(const std::vector<u64>& hashes);
|
||||
void AddTextureHashField(const QString& text = {});
|
||||
void EnsureTextureHashFieldRows();
|
||||
void SanitizeTextureHashField(QLineEdit* edit);
|
||||
void ShowTextureBrowser();
|
||||
void RefreshRuntimeElementSummary();
|
||||
void RefreshHandlingUi();
|
||||
void RefreshRefinementUi();
|
||||
void CaptureCurrentSeed();
|
||||
void AddCurrentHuntMatch();
|
||||
void RemoveSelectedMatchFilter();
|
||||
void OnAccept();
|
||||
|
||||
std::vector<std::string> m_available_flags;
|
||||
int m_edit_element_reference_total = 0;
|
||||
bool m_updating_texture_hash_fields = false;
|
||||
bool m_refinement_widgets_visible = false;
|
||||
|
||||
QLineEdit* m_name_edit = nullptr;
|
||||
QPlainTextEdit* m_comments_edit = nullptr;
|
||||
QLineEdit* m_credits_edit = nullptr;
|
||||
QComboBox* m_handling_combo = nullptr;
|
||||
QSpinBox* m_layer_spin = nullptr;
|
||||
QLabel* m_layer_label = nullptr;
|
||||
QDoubleSpinBox* m_element_depth_spin = nullptr;
|
||||
QLabel* m_element_depth_label = nullptr;
|
||||
QDoubleSpinBox* m_units_per_meter_spin = nullptr;
|
||||
QLabel* m_units_per_meter_label = nullptr;
|
||||
QLineEdit* m_flag_edit = nullptr;
|
||||
QLabel* m_flag_label = nullptr;
|
||||
QComboBox* m_condition_combo = nullptr;
|
||||
QLabel* m_condition_label = nullptr;
|
||||
QComboBox* m_condition_mode_combo = nullptr;
|
||||
QLabel* m_condition_mode_label = nullptr;
|
||||
QCheckBox* m_element_filter_check = nullptr;
|
||||
QSpinBox* m_element_start_spin = nullptr;
|
||||
QLabel* m_element_start_label = nullptr;
|
||||
QSpinBox* m_element_end_spin = nullptr;
|
||||
QLabel* m_element_end_label = nullptr;
|
||||
QPushButton* m_capture_seed_button = nullptr;
|
||||
QLabel* m_runtime_element_summary_label = nullptr;
|
||||
QCheckBox* m_runtime_use_projection_check = nullptr;
|
||||
QCheckBox* m_runtime_use_layer_check = nullptr;
|
||||
QCheckBox* m_runtime_use_viewport_check = nullptr;
|
||||
QCheckBox* m_runtime_use_scissor_check = nullptr;
|
||||
QCheckBox* m_runtime_use_render_state_check = nullptr;
|
||||
QComboBox* m_texture_mode_combo = nullptr;
|
||||
QComboBox* m_selected_match_mode_combo = nullptr;
|
||||
QPushButton* m_view_textures_button = nullptr;
|
||||
QWidget* m_texture_hash_container = nullptr;
|
||||
QScrollArea* m_texture_hash_scroll = nullptr;
|
||||
QVBoxLayout* m_texture_hash_layout = nullptr;
|
||||
std::vector<QLineEdit*> m_texture_hash_edits;
|
||||
QListWidget* m_selected_match_list = nullptr;
|
||||
QPushButton* m_add_current_match_button = nullptr;
|
||||
QPushButton* m_remove_selected_match_button = nullptr;
|
||||
QToolButton* m_refinement_toggle = nullptr;
|
||||
QFrame* m_refinement_frame = nullptr;
|
||||
QCheckBox* m_refinement_enable_check = nullptr;
|
||||
QComboBox* m_refinement_type_combo = nullptr;
|
||||
QCheckBox* m_refinement_family_check = nullptr;
|
||||
QLineEdit* m_refinement_hash_edit = nullptr;
|
||||
QLabel* m_refinement_hash_label = nullptr;
|
||||
QLineEdit* m_refinement_family_signature_edit = nullptr;
|
||||
QLabel* m_refinement_family_signature_label = nullptr;
|
||||
|
||||
ElementsGroupManager::RuntimeElementSignature m_runtime_element;
|
||||
std::vector<ElementsGroupManager::SelectedSubgroupSignature> m_selected_match_filters;
|
||||
bool m_selected_match_filters_excluded = false;
|
||||
};
|
||||
@@ -0,0 +1,409 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/ElementsGroupOverrideWidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "DolphinQt/Config/ElementsGroupOverrideAddEditDialog.h"
|
||||
#include "DolphinQt/Config/ElementsHuntingDialog.h"
|
||||
#include "DolphinQt/Config/ImportShaderFindElementDialog.h"
|
||||
#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
|
||||
#include "VideoCommon/ShaderHunter.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
QString FormatShaderOverrideImportLabel(const ShaderHunter::ShaderOverride& entry)
|
||||
{
|
||||
const QString type =
|
||||
entry.type == ShaderHunter::ShaderType::Vertex ? QStringLiteral("VS") :
|
||||
entry.type == ShaderHunter::ShaderType::Geometry ? QStringLiteral("GS") :
|
||||
QStringLiteral("PS");
|
||||
const QString handling =
|
||||
entry.handling == ShaderHunter::HandlingType::Screen ? QStringLiteral("screen") :
|
||||
entry.handling == ShaderHunter::HandlingType::Fullscreen ? QStringLiteral("fullscreen") :
|
||||
entry.handling == ShaderHunter::HandlingType::FullscreenMono ? QStringLiteral("fullscreen_mono") :
|
||||
entry.handling == ShaderHunter::HandlingType::HeadLocked ? QStringLiteral("headlocked") :
|
||||
entry.handling == ShaderHunter::HandlingType::Flag ? QStringLiteral("flag") :
|
||||
entry.handling == ShaderHunter::HandlingType::UnitsPerMeter ? QStringLiteral("units_per_meter") :
|
||||
QStringLiteral("skip");
|
||||
QString label = QStringLiteral("%1 [%2 %3]")
|
||||
.arg(QString::fromStdString(entry.name), type, handling);
|
||||
if (!entry.texture_hashes.empty())
|
||||
label += QStringLiteral(" tex[%1]").arg(entry.texture_hashes.size());
|
||||
if (!entry.enabled)
|
||||
label += QStringLiteral(" off");
|
||||
return label;
|
||||
}
|
||||
|
||||
std::optional<ShaderHunter::ShaderOverride> SelectShaderOverrideForImport(
|
||||
QWidget* parent, const std::vector<ShaderHunter::ShaderOverride>& overrides)
|
||||
{
|
||||
if (overrides.empty())
|
||||
return std::nullopt;
|
||||
|
||||
QDialog dialog(parent);
|
||||
dialog.setWindowTitle(QObject::tr("Import From Shader"));
|
||||
dialog.setMinimumWidth(420);
|
||||
|
||||
auto* list = new QListWidget(&dialog);
|
||||
for (size_t i = 0; i < overrides.size(); ++i)
|
||||
{
|
||||
auto* item = new QListWidgetItem(FormatShaderOverrideImportLabel(overrides[i]), list);
|
||||
item->setData(Qt::UserRole, static_cast<int>(i));
|
||||
}
|
||||
list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
if (list->count() > 0)
|
||||
list->setCurrentRow(0);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||||
auto* ok = buttons->button(QDialogButtonBox::Ok);
|
||||
if (ok != nullptr)
|
||||
ok->setText(QObject::tr("Import"));
|
||||
|
||||
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
|
||||
auto* layout = new QVBoxLayout(&dialog);
|
||||
layout->addWidget(new QLabel(QObject::tr("Choose a saved Shader Override to import."), &dialog));
|
||||
layout->addWidget(list);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted || list->currentItem() == nullptr)
|
||||
return std::nullopt;
|
||||
|
||||
const int index = list->currentItem()->data(Qt::UserRole).toInt();
|
||||
if (index < 0 || index >= static_cast<int>(overrides.size()))
|
||||
return std::nullopt;
|
||||
|
||||
return overrides[static_cast<size_t>(index)];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ElementsGroupOverrideWidget::ElementsGroupOverrideWidget(std::string game_id)
|
||||
: m_game_id(std::move(game_id))
|
||||
{
|
||||
CreateWidgets();
|
||||
ConnectWidgets();
|
||||
LoadOverrides();
|
||||
}
|
||||
|
||||
ElementsGroupOverrideWidget::~ElementsGroupOverrideWidget() = default;
|
||||
|
||||
void ElementsGroupOverrideWidget::CreateWidgets()
|
||||
{
|
||||
m_code_list = new QListWidget;
|
||||
|
||||
auto* info_label = new QLabel(
|
||||
tr("Elements Group Overrides use captured runtime draw signatures instead of only shader "
|
||||
"hashes.\nUse Elements Hunting to isolate frame-local draw groups, then save a new "
|
||||
"override."));
|
||||
info_label->setWordWrap(true);
|
||||
|
||||
m_code_add = new NonDefaultQPushButton(tr("&Add Override"));
|
||||
m_code_import = new NonDefaultQPushButton(tr("Import from Shader"));
|
||||
m_code_edit = new NonDefaultQPushButton(tr("&Edit Override"));
|
||||
m_code_remove = new NonDefaultQPushButton(tr("&Remove Override"));
|
||||
m_code_refresh = new NonDefaultQPushButton(tr("Re&fresh List"));
|
||||
m_code_reload = new NonDefaultQPushButton(tr("Reload &Overrides"));
|
||||
m_elements_hunting = new NonDefaultQPushButton(tr("Elements Hunting"));
|
||||
|
||||
m_code_edit->setEnabled(false);
|
||||
m_code_remove->setEnabled(false);
|
||||
|
||||
auto* button_layout = new QHBoxLayout;
|
||||
button_layout->addWidget(m_elements_hunting);
|
||||
button_layout->addWidget(m_code_add);
|
||||
button_layout->addWidget(m_code_import);
|
||||
button_layout->addWidget(m_code_edit);
|
||||
button_layout->addWidget(m_code_remove);
|
||||
button_layout->addStretch();
|
||||
button_layout->addWidget(m_code_refresh);
|
||||
button_layout->addWidget(m_code_reload);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->addWidget(info_label);
|
||||
layout->addWidget(m_code_list);
|
||||
layout->addLayout(button_layout);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::ConnectWidgets()
|
||||
{
|
||||
connect(m_code_list, &QListWidget::itemChanged, this, &ElementsGroupOverrideWidget::OnItemChanged);
|
||||
connect(m_code_list, &QListWidget::itemSelectionChanged, this,
|
||||
&ElementsGroupOverrideWidget::OnSelectionChanged);
|
||||
connect(m_code_add, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnAddClicked);
|
||||
connect(m_code_import, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnImportClicked);
|
||||
connect(m_code_edit, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnEditClicked);
|
||||
connect(m_code_remove, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnRemoveClicked);
|
||||
connect(m_code_refresh, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnRefreshClicked);
|
||||
connect(m_code_reload, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnReloadClicked);
|
||||
connect(m_elements_hunting, &QPushButton::clicked, this, &ElementsGroupOverrideWidget::OnHuntClicked);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::LoadOverrides()
|
||||
{
|
||||
m_overrides = ElementsGroupManager::LoadOverridesFromINI(m_game_id);
|
||||
UpdateList();
|
||||
m_code_edit->setEnabled(false);
|
||||
m_code_remove->setEnabled(false);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::SaveOverrides()
|
||||
{
|
||||
ElementsGroupManager::SaveOverridesToINI(m_game_id, m_overrides);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::UpdateList()
|
||||
{
|
||||
const QSignalBlocker blocker(m_code_list);
|
||||
m_code_list->clear();
|
||||
|
||||
for (size_t i = 0; i < m_overrides.size(); ++i)
|
||||
{
|
||||
const auto& entry = m_overrides[i];
|
||||
QString label = QStringLiteral("%1 (%2)")
|
||||
.arg(QString::fromStdString(entry.name))
|
||||
.arg(QString::fromLatin1(entry.handling == ShaderHunter::HandlingType::Screen ? "screen" :
|
||||
entry.handling == ShaderHunter::HandlingType::Fullscreen ? "fullscreen" :
|
||||
entry.handling == ShaderHunter::HandlingType::FullscreenMono ? "fullscreen_mono" :
|
||||
entry.handling == ShaderHunter::HandlingType::HeadLocked ? "headlocked" :
|
||||
entry.handling == ShaderHunter::HandlingType::Flag ? "flag" :
|
||||
entry.handling == ShaderHunter::HandlingType::UnitsPerMeter ? "units_per_meter" :
|
||||
"skip"));
|
||||
if (entry.runtime_element.use_projection)
|
||||
label += QStringLiteral(" proj");
|
||||
if (entry.runtime_element.use_layer)
|
||||
label += QStringLiteral(" layer");
|
||||
if (entry.runtime_element.use_viewport)
|
||||
label += QStringLiteral(" vp");
|
||||
if (entry.runtime_element.use_scissor)
|
||||
label += QStringLiteral(" scissor");
|
||||
if (entry.runtime_element.use_render_state)
|
||||
label += QStringLiteral(" state");
|
||||
if (!entry.texture_hashes.empty())
|
||||
label += QStringLiteral(" tex");
|
||||
if (!entry.selected_match_filter.empty())
|
||||
{
|
||||
label += QStringLiteral(" %1[%2]")
|
||||
.arg(entry.selected_match_filter_excluded ? QStringLiteral("match_exclude") :
|
||||
QStringLiteral("matches"))
|
||||
.arg(entry.selected_match_filter.size());
|
||||
}
|
||||
if (entry.element_start >= 0 && entry.element_end >= 0)
|
||||
label += QStringLiteral(" draw[%1-%2]").arg(entry.element_start).arg(entry.element_end);
|
||||
if (!entry.condition_flag.empty())
|
||||
label += QStringLiteral(" when(%1%2)")
|
||||
.arg(entry.condition_inverted ? QStringLiteral("!") : QString())
|
||||
.arg(QString::fromStdString(entry.condition_flag));
|
||||
if (entry.refinement_enabled)
|
||||
{
|
||||
const QString refine_type =
|
||||
entry.refinement_type == ShaderHunter::ShaderType::Vertex ? QStringLiteral("VS") :
|
||||
entry.refinement_type == ShaderHunter::ShaderType::Geometry ? QStringLiteral("GS") :
|
||||
QStringLiteral("PS");
|
||||
label += QStringLiteral(" refine[%1 %2]")
|
||||
.arg(refine_type)
|
||||
.arg(entry.refinement_family_match ? QStringLiteral("family") :
|
||||
QStringLiteral("exact"));
|
||||
}
|
||||
|
||||
auto* item = new QListWidgetItem(label);
|
||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(entry.enabled ? Qt::Checked : Qt::Unchecked);
|
||||
item->setData(Qt::UserRole, static_cast<int>(i));
|
||||
m_code_list->addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ElementsGroupOverrideWidget::CollectAvailableFlags() const
|
||||
{
|
||||
std::vector<std::string> flags;
|
||||
const auto shader_overrides = ShaderHunter::LoadOverridesFromINI(m_game_id);
|
||||
for (const auto& entry : shader_overrides)
|
||||
{
|
||||
if (!entry.flag_group.empty() &&
|
||||
std::find(flags.begin(), flags.end(), entry.flag_group) == flags.end())
|
||||
{
|
||||
flags.push_back(entry.flag_group);
|
||||
}
|
||||
}
|
||||
for (const auto& entry : m_overrides)
|
||||
{
|
||||
if (!entry.flag_group.empty() &&
|
||||
std::find(flags.begin(), flags.end(), entry.flag_group) == flags.end())
|
||||
{
|
||||
flags.push_back(entry.flag_group);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnItemChanged(QListWidgetItem* item)
|
||||
{
|
||||
const int idx = item->data(Qt::UserRole).toInt();
|
||||
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
|
||||
return;
|
||||
m_overrides[idx].enabled = item->checkState() == Qt::Checked;
|
||||
INFO_LOG_FMT(VIDEO, "ElementsGroup: override '{}' {}", m_overrides[idx].name,
|
||||
m_overrides[idx].enabled ? "enabled" : "disabled");
|
||||
SaveOverrides();
|
||||
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnSelectionChanged()
|
||||
{
|
||||
const bool has_selection = !m_code_list->selectedItems().empty();
|
||||
m_code_edit->setEnabled(has_selection);
|
||||
m_code_remove->setEnabled(has_selection);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnAddClicked()
|
||||
{
|
||||
ElementsGroupOverrideAddEditDialog dialog(this, nullptr, CollectAvailableFlags());
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
auto result = dialog.GetResult();
|
||||
result.enabled = true;
|
||||
result.user_defined = true;
|
||||
m_overrides.push_back(std::move(result));
|
||||
SaveOverrides();
|
||||
LoadOverrides();
|
||||
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnImportClicked()
|
||||
{
|
||||
std::vector<ShaderHunter::ShaderOverride> supported_overrides;
|
||||
for (const auto& entry : ShaderHunter::LoadOverridesFromINI(m_game_id))
|
||||
{
|
||||
if (entry.match_mode == ShaderHunter::MatchMode::RuntimeElement)
|
||||
continue;
|
||||
supported_overrides.push_back(entry);
|
||||
}
|
||||
|
||||
if (supported_overrides.empty())
|
||||
{
|
||||
QMessageBox::information(this, tr("Import from Shader"),
|
||||
tr("No compatible saved Shader Overrides were found."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto selected = SelectShaderOverrideForImport(this, supported_overrides);
|
||||
if (!selected.has_value())
|
||||
return;
|
||||
|
||||
ImportShaderFindElementDialog find_dialog(m_game_id, *selected, this);
|
||||
const int result_code = find_dialog.exec();
|
||||
if (find_dialog.ShouldOpenElementsHunting())
|
||||
{
|
||||
OnHuntClicked();
|
||||
return;
|
||||
}
|
||||
if (result_code != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
const auto selected_candidate = find_dialog.GetSelectedCandidate();
|
||||
if (!selected_candidate.has_value())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Import from Shader"),
|
||||
tr("No runtime element seed candidate was selected."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto import_runtime_element =
|
||||
ElementsGroupManager::MakeSelectedMatchFilterSignature(selected_candidate->signature);
|
||||
ElementsGroupOverrideAddEditDialog dialog(this, nullptr, CollectAvailableFlags(), &*selected,
|
||||
&import_runtime_element);
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
auto result = dialog.GetResult();
|
||||
result.enabled = true;
|
||||
result.user_defined = true;
|
||||
m_overrides.push_back(std::move(result));
|
||||
SaveOverrides();
|
||||
LoadOverrides();
|
||||
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnEditClicked()
|
||||
{
|
||||
const auto items = m_code_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
const int idx = items[0]->data(Qt::UserRole).toInt();
|
||||
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
|
||||
return;
|
||||
|
||||
ElementsGroupOverrideAddEditDialog dialog(this, &m_overrides[idx], CollectAvailableFlags());
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
auto result = dialog.GetResult();
|
||||
result.enabled = m_overrides[idx].enabled;
|
||||
m_overrides[idx] = std::move(result);
|
||||
SaveOverrides();
|
||||
LoadOverrides();
|
||||
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnRemoveClicked()
|
||||
{
|
||||
const auto items = m_code_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
const int idx = items[0]->data(Qt::UserRole).toInt();
|
||||
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
|
||||
return;
|
||||
|
||||
m_overrides.erase(m_overrides.begin() + idx);
|
||||
SaveOverrides();
|
||||
LoadOverrides();
|
||||
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnRefreshClicked()
|
||||
{
|
||||
LoadOverrides();
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnReloadClicked()
|
||||
{
|
||||
INFO_LOG_FMT(VIDEO, "ElementsGroup: reload requested for {}", m_game_id);
|
||||
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ElementsGroupOverrideWidget::OnHuntClicked()
|
||||
{
|
||||
if (m_hunting_dialog)
|
||||
{
|
||||
m_hunting_dialog->show();
|
||||
m_hunting_dialog->raise();
|
||||
m_hunting_dialog->activateWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
m_hunting_dialog = new ElementsHuntingDialog(m_game_id, this);
|
||||
m_hunting_dialog->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
connect(m_hunting_dialog, &ElementsHuntingDialog::OverridesChanged, this,
|
||||
[this]() {
|
||||
LoadOverrides();
|
||||
ElementsGroupManager::GetInstance().LoadOverrides(m_game_id);
|
||||
});
|
||||
connect(m_hunting_dialog, &QObject::destroyed, this,
|
||||
[this]() { m_hunting_dialog = nullptr; });
|
||||
m_hunting_dialog->show();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPointer>
|
||||
#include <QWidget>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "VideoCommon/ElementsGroupManager.h"
|
||||
|
||||
class ElementsHuntingDialog;
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QPushButton;
|
||||
|
||||
class ElementsGroupOverrideWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ElementsGroupOverrideWidget(std::string game_id);
|
||||
~ElementsGroupOverrideWidget() override;
|
||||
|
||||
private:
|
||||
void CreateWidgets();
|
||||
void ConnectWidgets();
|
||||
void UpdateList();
|
||||
void LoadOverrides();
|
||||
void SaveOverrides();
|
||||
std::vector<std::string> CollectAvailableFlags() const;
|
||||
|
||||
void OnItemChanged(QListWidgetItem* item);
|
||||
void OnSelectionChanged();
|
||||
void OnAddClicked();
|
||||
void OnImportClicked();
|
||||
void OnEditClicked();
|
||||
void OnRemoveClicked();
|
||||
void OnRefreshClicked();
|
||||
void OnReloadClicked();
|
||||
void OnHuntClicked();
|
||||
|
||||
std::string m_game_id;
|
||||
QListWidget* m_code_list = nullptr;
|
||||
QPushButton* m_code_add = nullptr;
|
||||
QPushButton* m_code_import = nullptr;
|
||||
QPushButton* m_code_edit = nullptr;
|
||||
QPushButton* m_code_remove = nullptr;
|
||||
QPushButton* m_code_refresh = nullptr;
|
||||
QPushButton* m_code_reload = nullptr;
|
||||
QPushButton* m_elements_hunting = nullptr;
|
||||
QPointer<ElementsHuntingDialog> m_hunting_dialog;
|
||||
std::vector<ElementsGroupManager::ElementGroupOverride> m_overrides;
|
||||
};
|
||||
@@ -0,0 +1,721 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/ElementsHuntingDialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QAbstractItemView>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "DolphinQt/Config/ElementsGroupOverrideAddEditDialog.h"
|
||||
#include "DolphinQt/Config/TextureHashBrowserDialog.h"
|
||||
#include "VideoCommon/ElementsGroupManager.h"
|
||||
#include "VideoCommon/ShaderHunter.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
QString GetProjectionLabel(const ShaderHunter::RuntimeElementSignature& sig)
|
||||
{
|
||||
if (sig.perspective)
|
||||
{
|
||||
return QObject::tr("Perspective %1x%2")
|
||||
.arg(sig.perspective_hfov_x100 / 100.0, 0, 'f', 2)
|
||||
.arg(sig.perspective_vfov_x100 / 100.0, 0, 'f', 2);
|
||||
}
|
||||
|
||||
return QObject::tr("Ortho L%1")
|
||||
.arg(sig.ortho_layer);
|
||||
}
|
||||
|
||||
QString FormatSeedCandidateLabel(const ElementsGroupManager::SeedCandidate& candidate,
|
||||
const ShaderHunter::RuntimeElementSignature& active_mask)
|
||||
{
|
||||
const auto& group = candidate.group_signature;
|
||||
const auto& rep = candidate.representative_draw.signature;
|
||||
QStringList parts;
|
||||
|
||||
if (!active_mask.use_projection && !active_mask.use_layer && !active_mask.use_viewport &&
|
||||
!active_mask.use_scissor && !active_mask.use_render_state)
|
||||
{
|
||||
parts << GetProjectionLabel(candidate.signature);
|
||||
parts << QObject::tr("VP %1,%2 %3x%4")
|
||||
.arg(candidate.signature.viewport_x)
|
||||
.arg(candidate.signature.viewport_y)
|
||||
.arg(candidate.signature.viewport_width)
|
||||
.arg(candidate.signature.viewport_height);
|
||||
parts << QObject::tr("SC %1,%2 %3,%4")
|
||||
.arg(candidate.signature.scissor_left)
|
||||
.arg(candidate.signature.scissor_top)
|
||||
.arg(candidate.signature.scissor_right)
|
||||
.arg(candidate.signature.scissor_bottom);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (active_mask.use_projection)
|
||||
parts << GetProjectionLabel(group);
|
||||
else if (active_mask.use_layer)
|
||||
parts << (rep.perspective ? QObject::tr("Perspective") :
|
||||
QObject::tr("Ortho L%1").arg(group.ortho_layer));
|
||||
|
||||
if (active_mask.use_viewport)
|
||||
{
|
||||
parts << QObject::tr("VP %1,%2 %3x%4")
|
||||
.arg(group.viewport_x)
|
||||
.arg(group.viewport_y)
|
||||
.arg(group.viewport_width)
|
||||
.arg(group.viewport_height);
|
||||
}
|
||||
|
||||
if (active_mask.use_scissor)
|
||||
{
|
||||
parts << QObject::tr("SC %1,%2 %3,%4")
|
||||
.arg(group.scissor_left)
|
||||
.arg(group.scissor_top)
|
||||
.arg(group.scissor_right)
|
||||
.arg(group.scissor_bottom);
|
||||
}
|
||||
|
||||
if (active_mask.use_render_state)
|
||||
parts << QObject::tr("State");
|
||||
}
|
||||
|
||||
parts << QObject::tr("Draw #%1").arg(candidate.representative_draw.draw_index + 1);
|
||||
parts << QObject::tr("Seen %1x").arg(candidate.occurrence_count);
|
||||
return parts.join(QStringLiteral(" | "));
|
||||
}
|
||||
|
||||
QString FormatSeedSummary(const ElementsGroupManager::SeedCandidate& candidate,
|
||||
const ShaderHunter::RuntimeElementSignature& active_mask)
|
||||
{
|
||||
return QObject::tr("%1\nRepresentative Draw: #%2\nOccurrences: %3")
|
||||
.arg(FormatSeedCandidateLabel(candidate, active_mask))
|
||||
.arg(candidate.representative_draw.draw_index + 1)
|
||||
.arg(candidate.occurrence_count);
|
||||
}
|
||||
|
||||
QString FormatMatchLabel(const ElementsGroupManager::DrawRecord& draw)
|
||||
{
|
||||
return QObject::tr("Draw #%1 | VS %2 | PS %3 | GS %4")
|
||||
.arg(draw.draw_index + 1)
|
||||
.arg(static_cast<qulonglong>(draw.vs_hash), 16, 16, QLatin1Char('0'))
|
||||
.arg(static_cast<qulonglong>(draw.ps_hash), 16, 16, QLatin1Char('0'))
|
||||
.arg(static_cast<qulonglong>(draw.gs_hash), 16, 16, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
QString FormatTextureSummary(const std::vector<u64>& textures)
|
||||
{
|
||||
if (textures.empty())
|
||||
return QObject::tr("No textures");
|
||||
|
||||
QStringList parts;
|
||||
const int shown = std::min<int>(2, static_cast<int>(textures.size()));
|
||||
for (int i = 0; i < shown; ++i)
|
||||
{
|
||||
parts << QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(textures[static_cast<size_t>(i)]), 16, 16,
|
||||
QLatin1Char('0'));
|
||||
}
|
||||
if (static_cast<int>(textures.size()) > shown)
|
||||
parts << QObject::tr("+%1 more").arg(static_cast<int>(textures.size()) - shown);
|
||||
return parts.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
QString FormatCurrentMatchLabel(const ElementsGroupManager::CurrentMatchCandidate& candidate)
|
||||
{
|
||||
QString label = GetProjectionLabel(candidate.representative_draw.signature);
|
||||
label += QObject::tr(" | F %1/%2/%3")
|
||||
.arg(QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(candidate.subgroup.vs_family), 16, 8,
|
||||
QLatin1Char('0')))
|
||||
.arg(QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(candidate.subgroup.ps_family), 16, 8,
|
||||
QLatin1Char('0')))
|
||||
.arg(QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(candidate.subgroup.gs_family), 16, 8,
|
||||
QLatin1Char('0')));
|
||||
label += QObject::tr(" | Tex %1").arg(FormatTextureSummary(candidate.subgroup.texture_hashes));
|
||||
label += QObject::tr(" | x%1").arg(candidate.raw_draw_count);
|
||||
if (candidate.representative_draw.draw_index >= 0)
|
||||
label += QObject::tr(" | Draw #%1").arg(candidate.representative_draw.draw_index + 1);
|
||||
if (!candidate.active_this_frame)
|
||||
label += QObject::tr(" | Inactive");
|
||||
return label;
|
||||
}
|
||||
|
||||
QString GetPreviewScopeLabel(const ElementsGroupManager::Status& status)
|
||||
{
|
||||
if (status.selected_match_filter_count <= 0)
|
||||
return QObject::tr("Whole current group");
|
||||
|
||||
return status.selected_match_filter_excluded ? QObject::tr("Unchecked matches only") :
|
||||
QObject::tr("Checked matches only");
|
||||
}
|
||||
|
||||
std::vector<TextureHashBrowserEntry> CollectTextureBrowserEntries(
|
||||
const std::vector<ElementsGroupManager::DrawRecord>& draws)
|
||||
{
|
||||
std::vector<TextureHashBrowserEntry> entries;
|
||||
std::unordered_map<u64, size_t> indices;
|
||||
|
||||
for (const auto& draw : draws)
|
||||
{
|
||||
for (size_t i = 0; i < draw.textures.size(); ++i)
|
||||
{
|
||||
const u64 hash = draw.textures[i];
|
||||
if (hash == 0)
|
||||
continue;
|
||||
|
||||
const auto it = indices.find(hash);
|
||||
if (it == indices.end())
|
||||
{
|
||||
indices.emplace(hash, entries.size());
|
||||
entries.push_back(TextureHashBrowserEntry{.hash = hash, .name = draw.texture_names[i]});
|
||||
}
|
||||
else if (entries[it->second].name.empty() && !draw.texture_names[i].empty())
|
||||
{
|
||||
entries[it->second].name = draw.texture_names[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const TextureHashBrowserEntry& a, const TextureHashBrowserEntry& b) {
|
||||
return a.hash < b.hash;
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectAvailableFlags(const std::string& game_id)
|
||||
{
|
||||
std::vector<std::string> flags;
|
||||
const auto shader_overrides = ShaderHunter::LoadOverridesFromINI(game_id);
|
||||
for (const auto& entry : shader_overrides)
|
||||
{
|
||||
if (!entry.flag_group.empty() &&
|
||||
std::find(flags.begin(), flags.end(), entry.flag_group) == flags.end())
|
||||
{
|
||||
flags.push_back(entry.flag_group);
|
||||
}
|
||||
}
|
||||
const auto element_overrides = ElementsGroupManager::LoadOverridesFromINI(game_id);
|
||||
for (const auto& entry : element_overrides)
|
||||
{
|
||||
if (!entry.flag_group.empty() &&
|
||||
std::find(flags.begin(), flags.end(), entry.flag_group) == flags.end())
|
||||
{
|
||||
flags.push_back(entry.flag_group);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ElementsHuntingDialog::ElementsHuntingDialog(std::string game_id, QWidget* parent)
|
||||
: QDialog(parent), m_game_id(std::move(game_id))
|
||||
{
|
||||
setWindowTitle(tr("Elements Hunting"));
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setMinimumWidth(420);
|
||||
|
||||
ElementsGroupManager::GetInstance().SetPopupOpen(true);
|
||||
if (!m_game_id.empty())
|
||||
ElementsGroupManager::GetInstance().LoadOverridesIfNeeded(m_game_id);
|
||||
|
||||
CreateWidgets();
|
||||
ConnectWidgets();
|
||||
RefreshPendingTextureSummary();
|
||||
|
||||
m_update_timer = new QTimer(this);
|
||||
connect(m_update_timer, &QTimer::timeout, this, &ElementsHuntingDialog::UpdateDisplay);
|
||||
m_update_timer->start(100);
|
||||
}
|
||||
|
||||
ElementsHuntingDialog::~ElementsHuntingDialog() = default;
|
||||
|
||||
void ElementsHuntingDialog::CreateWidgets()
|
||||
{
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
|
||||
auto* info = new QLabel(
|
||||
tr("Select a runtime-signature seed candidate, then enable signature groups to cycle "
|
||||
"matching elements across the current frame.\n"
|
||||
"Preview affects the whole current group until one or more matches are checked below. "
|
||||
"After that, preview and saved overrides affect only the checked matches.\n"
|
||||
"Seen matches stay in the list even when not drawn and are shown in grey."));
|
||||
info->setWordWrap(true);
|
||||
layout->addWidget(info);
|
||||
|
||||
m_enable_check = new QCheckBox(tr("Enable Group Hunt"));
|
||||
layout->addWidget(m_enable_check);
|
||||
|
||||
auto* option_layout = new QHBoxLayout;
|
||||
option_layout->addWidget(new QLabel(tr("Preview:")));
|
||||
m_option_combo = new QComboBox;
|
||||
m_option_combo->addItem(tr("Skip"), static_cast<int>(ShaderHunter::HuntingOption::Skip));
|
||||
m_option_combo->addItem(tr("Pink"), static_cast<int>(ShaderHunter::HuntingOption::Pink));
|
||||
option_layout->addWidget(m_option_combo);
|
||||
option_layout->addWidget(new QLabel(tr("Match Mode:")));
|
||||
m_match_filter_mode_combo = new QComboBox;
|
||||
m_match_filter_mode_combo->addItem(tr("Include"), false);
|
||||
m_match_filter_mode_combo->addItem(tr("Exclude"), true);
|
||||
option_layout->addWidget(m_match_filter_mode_combo);
|
||||
option_layout->addWidget(new QLabel(tr("Texture Mode:")));
|
||||
m_texture_mode_combo = new QComboBox;
|
||||
m_texture_mode_combo->addItem(tr("Include"), false);
|
||||
m_texture_mode_combo->addItem(tr("Exclude"), true);
|
||||
option_layout->addWidget(m_texture_mode_combo);
|
||||
option_layout->addStretch();
|
||||
layout->addLayout(option_layout);
|
||||
|
||||
layout->addWidget(new QLabel(tr("Seed Candidates:")));
|
||||
m_seed_list = new QListWidget;
|
||||
m_seed_list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
layout->addWidget(m_seed_list);
|
||||
|
||||
m_seed_label = new QLabel(tr("Seed: (none)"));
|
||||
m_seed_label->setWordWrap(true);
|
||||
layout->addWidget(m_seed_label);
|
||||
|
||||
auto* groups_layout = new QHBoxLayout;
|
||||
m_projection_check = new QCheckBox(tr("Projection"));
|
||||
m_layer_check = new QCheckBox(tr("Layer"));
|
||||
m_viewport_check = new QCheckBox(tr("Viewport"));
|
||||
m_scissor_check = new QCheckBox(tr("Scissor"));
|
||||
m_render_state_check = new QCheckBox(tr("Render State"));
|
||||
groups_layout->addWidget(m_projection_check);
|
||||
groups_layout->addWidget(m_layer_check);
|
||||
groups_layout->addWidget(m_viewport_check);
|
||||
groups_layout->addWidget(m_scissor_check);
|
||||
groups_layout->addWidget(m_render_state_check);
|
||||
layout->addLayout(groups_layout);
|
||||
|
||||
m_match_label = new QLabel(tr("Matches: - / -"));
|
||||
layout->addWidget(m_match_label);
|
||||
|
||||
auto* match_nav = new QHBoxLayout;
|
||||
m_prev_match_button = new QPushButton(tr("<< Previous Match"));
|
||||
m_next_match_button = new QPushButton(tr("Next Match >>"));
|
||||
match_nav->addWidget(m_prev_match_button);
|
||||
match_nav->addWidget(m_next_match_button);
|
||||
layout->addLayout(match_nav);
|
||||
|
||||
layout->addWidget(new QLabel(tr("Current Matches:")));
|
||||
m_current_match_list = new QListWidget;
|
||||
m_current_match_list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
layout->addWidget(m_current_match_list);
|
||||
|
||||
auto* texture_layout = new QHBoxLayout;
|
||||
m_view_textures_button = new QPushButton(tr("View Textures"));
|
||||
texture_layout->addWidget(m_view_textures_button);
|
||||
texture_layout->addStretch();
|
||||
layout->addLayout(texture_layout);
|
||||
|
||||
m_pending_texture_label = new QLabel;
|
||||
m_pending_texture_label->setWordWrap(true);
|
||||
layout->addWidget(m_pending_texture_label);
|
||||
|
||||
auto* refresh_layout = new QHBoxLayout;
|
||||
m_auto_refresh_check = new QCheckBox(tr("Auto Refresh"));
|
||||
m_auto_refresh_check->setToolTip(
|
||||
tr("Continuously refresh the current matches list while this window is open.\n"
|
||||
"Disable this when the window becomes sluggish and use Refresh manually."));
|
||||
m_refresh_button = new QPushButton(tr("Refresh"));
|
||||
refresh_layout->addWidget(m_auto_refresh_check);
|
||||
refresh_layout->addStretch();
|
||||
refresh_layout->addWidget(m_refresh_button);
|
||||
layout->addLayout(refresh_layout);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
|
||||
m_save_button = buttons->addButton(tr("Save To Elements Group Override"),
|
||||
QDialogButtonBox::ActionRole);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||
layout->addWidget(buttons);
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::ConnectWidgets()
|
||||
{
|
||||
auto& manager = ElementsGroupManager::GetInstance();
|
||||
connect(m_enable_check, &QCheckBox::toggled, this, [&manager](bool checked) {
|
||||
manager.SetHuntEnabled(checked);
|
||||
});
|
||||
connect(m_option_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
[&manager, this]() {
|
||||
manager.SetHuntingOption(static_cast<ShaderHunter::HuntingOption>(
|
||||
m_option_combo->currentData().toInt()));
|
||||
});
|
||||
connect(m_match_filter_mode_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
[&manager, this]() {
|
||||
manager.SetSelectedMatchFilterExcluded(m_match_filter_mode_combo->currentData().toBool());
|
||||
RequestRefresh();
|
||||
});
|
||||
connect(m_texture_mode_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
[this]() { RefreshPendingTextureSummary(); });
|
||||
connect(m_refresh_button, &QPushButton::clicked, this, &ElementsHuntingDialog::RequestRefresh);
|
||||
connect(m_auto_refresh_check, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
if (checked)
|
||||
RequestRefresh();
|
||||
});
|
||||
connect(m_seed_list, &QListWidget::itemSelectionChanged, this,
|
||||
&ElementsHuntingDialog::OnSeedSelectionChanged);
|
||||
connect(m_view_textures_button, &QPushButton::clicked, this,
|
||||
&ElementsHuntingDialog::ShowTextureBrowser);
|
||||
|
||||
const auto apply_groups = [this, &manager]() {
|
||||
manager.SetSeedGroupMask(m_projection_check->isChecked(), m_layer_check->isChecked(),
|
||||
m_viewport_check->isChecked(), m_scissor_check->isChecked(),
|
||||
m_render_state_check->isChecked());
|
||||
RequestRefresh();
|
||||
};
|
||||
for (QCheckBox* checkbox : {m_projection_check, m_layer_check, m_viewport_check,
|
||||
m_scissor_check, m_render_state_check})
|
||||
{
|
||||
connect(checkbox, &QCheckBox::toggled, this, apply_groups);
|
||||
}
|
||||
|
||||
connect(m_prev_match_button, &QPushButton::clicked, this,
|
||||
[this, &manager]() {
|
||||
manager.PrevMatch();
|
||||
RequestRefresh();
|
||||
});
|
||||
connect(m_next_match_button, &QPushButton::clicked, this,
|
||||
[this, &manager]() {
|
||||
manager.NextMatch();
|
||||
RequestRefresh();
|
||||
});
|
||||
connect(m_current_match_list, &QListWidget::itemSelectionChanged, this,
|
||||
&ElementsHuntingDialog::OnCurrentMatchSelectionChanged);
|
||||
connect(m_current_match_list, &QListWidget::itemChanged, this,
|
||||
&ElementsHuntingDialog::OnCurrentMatchItemChanged);
|
||||
connect(m_save_button, &QPushButton::clicked, this, &ElementsHuntingDialog::SaveCurrentOverride);
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::UpdateDisplay()
|
||||
{
|
||||
auto& manager = ElementsGroupManager::GetInstance();
|
||||
const auto status = manager.GetStatus();
|
||||
const bool refresh_lists = m_refresh_pending || (m_auto_refresh_check && m_auto_refresh_check->isChecked());
|
||||
|
||||
{
|
||||
const QSignalBlocker blocker(m_enable_check);
|
||||
m_enable_check->setChecked(status.hunt_enabled);
|
||||
}
|
||||
{
|
||||
const QSignalBlocker blocker(m_option_combo);
|
||||
const int idx = m_option_combo->findData(static_cast<int>(status.option));
|
||||
if (idx >= 0)
|
||||
m_option_combo->setCurrentIndex(idx);
|
||||
}
|
||||
{
|
||||
const QSignalBlocker blocker(m_match_filter_mode_combo);
|
||||
const int idx = m_match_filter_mode_combo->findData(status.selected_match_filter_excluded);
|
||||
if (idx >= 0)
|
||||
m_match_filter_mode_combo->setCurrentIndex(idx);
|
||||
}
|
||||
|
||||
const auto candidates = manager.GetSeedCandidates();
|
||||
if (refresh_lists)
|
||||
{
|
||||
{
|
||||
const QSignalBlocker blocker(m_seed_list);
|
||||
if (m_seed_list->count() != static_cast<int>(candidates.size()))
|
||||
{
|
||||
m_seed_list->clear();
|
||||
for (size_t i = 0; i < candidates.size(); ++i)
|
||||
{
|
||||
auto* item = new QListWidgetItem(FormatSeedCandidateLabel(candidates[i], status.seed_signature));
|
||||
item->setData(Qt::UserRole, static_cast<int>(i));
|
||||
m_seed_list->addItem(item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < m_seed_list->count(); ++i)
|
||||
m_seed_list->item(i)->setText(
|
||||
FormatSeedCandidateLabel(candidates[static_cast<size_t>(i)], status.seed_signature));
|
||||
}
|
||||
|
||||
if (status.selected_seed_index >= 0 && status.selected_seed_index < m_seed_list->count())
|
||||
m_seed_list->setCurrentRow(status.selected_seed_index);
|
||||
else
|
||||
m_seed_list->clearSelection();
|
||||
}
|
||||
|
||||
const auto current_matches = manager.GetCurrentMatches();
|
||||
{
|
||||
const QSignalBlocker blocker(m_current_match_list);
|
||||
if (m_current_match_list->count() != static_cast<int>(current_matches.size()))
|
||||
{
|
||||
m_current_match_list->clear();
|
||||
for (size_t i = 0; i < current_matches.size(); ++i)
|
||||
{
|
||||
auto* item = new QListWidgetItem(FormatCurrentMatchLabel(current_matches[i]));
|
||||
item->setData(Qt::UserRole, static_cast<int>(i));
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(manager.IsCurrentMatchFilterEnabled(static_cast<int>(i)) ? Qt::Checked :
|
||||
Qt::Unchecked);
|
||||
if (!current_matches[i].active_this_frame)
|
||||
item->setForeground(palette().color(QPalette::Disabled, QPalette::Text));
|
||||
m_current_match_list->addItem(item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < m_current_match_list->count(); ++i)
|
||||
{
|
||||
auto* item = m_current_match_list->item(i);
|
||||
item->setText(FormatCurrentMatchLabel(current_matches[static_cast<size_t>(i)]));
|
||||
item->setCheckState(manager.IsCurrentMatchFilterEnabled(i) ? Qt::Checked : Qt::Unchecked);
|
||||
item->setForeground(current_matches[static_cast<size_t>(i)].active_this_frame ?
|
||||
palette().color(QPalette::Text) :
|
||||
palette().color(QPalette::Disabled, QPalette::Text));
|
||||
}
|
||||
}
|
||||
|
||||
if (status.selected_match >= 0 && status.selected_match < m_current_match_list->count())
|
||||
m_current_match_list->setCurrentRow(status.selected_match);
|
||||
else
|
||||
m_current_match_list->clearSelection();
|
||||
}
|
||||
|
||||
m_refresh_pending = false;
|
||||
}
|
||||
|
||||
if (status.seed_valid)
|
||||
{
|
||||
QStringList groups;
|
||||
if (status.seed_signature.use_projection)
|
||||
groups << tr("Projection");
|
||||
if (status.seed_signature.use_layer)
|
||||
groups << tr("Layer");
|
||||
if (status.seed_signature.use_viewport)
|
||||
groups << tr("Viewport");
|
||||
if (status.seed_signature.use_scissor)
|
||||
groups << tr("Scissor");
|
||||
if (status.seed_signature.use_render_state)
|
||||
groups << tr("Render State");
|
||||
QString seed_summary = tr("Seed: %1\nGroups: %2\nChecked Match Filters: %3")
|
||||
.arg(status.selected_seed_index >= 0 &&
|
||||
status.selected_seed_index < static_cast<int>(candidates.size()) ?
|
||||
FormatSeedSummary(candidates[static_cast<size_t>(status.selected_seed_index)],
|
||||
status.seed_signature) :
|
||||
tr("selected"))
|
||||
.arg(groups.isEmpty() ? tr("(none)") :
|
||||
groups.join(QStringLiteral(", ")))
|
||||
.arg(status.selected_match_filter_count);
|
||||
if (status.selected_match_filter_count > 0)
|
||||
{
|
||||
seed_summary += tr("\nMatch Mode: %1")
|
||||
.arg(status.selected_match_filter_excluded ? tr("Exclude") :
|
||||
tr("Include"));
|
||||
}
|
||||
m_seed_label->setText(seed_summary);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_seed_label->setText(tr("Seed: (none)"));
|
||||
}
|
||||
|
||||
{
|
||||
const QSignalBlocker p(m_projection_check);
|
||||
const QSignalBlocker l(m_layer_check);
|
||||
const QSignalBlocker v(m_viewport_check);
|
||||
const QSignalBlocker s(m_scissor_check);
|
||||
const QSignalBlocker r(m_render_state_check);
|
||||
m_projection_check->setChecked(status.seed_signature.use_projection);
|
||||
m_layer_check->setChecked(status.seed_signature.use_layer);
|
||||
m_viewport_check->setChecked(status.seed_signature.use_viewport);
|
||||
m_scissor_check->setChecked(status.seed_signature.use_scissor);
|
||||
m_render_state_check->setChecked(status.seed_signature.use_render_state);
|
||||
}
|
||||
|
||||
if (status.highlighted_draw)
|
||||
{
|
||||
m_match_label->setText(tr("Matches: %1 / %2 (Active: %3)\nCurrent Match: %4\nPreview Scope: %5")
|
||||
.arg(status.total_matches > 0 ? status.selected_match + 1 : 0)
|
||||
.arg(status.total_matches)
|
||||
.arg(status.active_matches)
|
||||
.arg(tr("%1 | Covers %2 draw(s)")
|
||||
.arg(FormatMatchLabel(*status.highlighted_draw))
|
||||
.arg(status.highlighted_match_raw_draw_count))
|
||||
.arg(GetPreviewScopeLabel(status)));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_match_label->setText(tr("Matches: %1 / %2 (Active: %3)\nPreview Scope: %4")
|
||||
.arg(status.total_matches > 0 ? status.selected_match + 1 : 0)
|
||||
.arg(status.total_matches)
|
||||
.arg(status.active_matches)
|
||||
.arg(GetPreviewScopeLabel(status)));
|
||||
}
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::RequestRefresh()
|
||||
{
|
||||
m_refresh_pending = true;
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::RefreshPendingTextureSummary()
|
||||
{
|
||||
if (m_pending_texture_hashes.empty())
|
||||
{
|
||||
m_pending_texture_label->setText(tr("Pending Texture Filters: (none)"));
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList hashes;
|
||||
const int shown = std::min<int>(3, static_cast<int>(m_pending_texture_hashes.size()));
|
||||
for (int i = 0; i < shown; ++i)
|
||||
{
|
||||
hashes << QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(m_pending_texture_hashes[static_cast<size_t>(i)]), 16,
|
||||
16, QLatin1Char('0'));
|
||||
}
|
||||
if (static_cast<int>(m_pending_texture_hashes.size()) > shown)
|
||||
{
|
||||
hashes << tr("+%1 more").arg(static_cast<int>(m_pending_texture_hashes.size()) - shown);
|
||||
}
|
||||
|
||||
m_pending_texture_label->setText(
|
||||
tr("Pending Texture %1 Filter(s): %2")
|
||||
.arg(m_texture_mode_combo->currentData().toBool() ? tr("Exclude") : tr("Include"))
|
||||
.arg(hashes.join(QStringLiteral(", "))));
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::OnSeedSelectionChanged()
|
||||
{
|
||||
const auto items = m_seed_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
|
||||
const int index = items[0]->data(Qt::UserRole).toInt();
|
||||
ElementsGroupManager::GetInstance().SelectSeedCandidate(index);
|
||||
m_pending_texture_hashes.clear();
|
||||
RefreshPendingTextureSummary();
|
||||
RequestRefresh();
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::ShowTextureBrowser()
|
||||
{
|
||||
const auto status = ElementsGroupManager::GetInstance().GetStatus();
|
||||
if (!status.seed_valid)
|
||||
{
|
||||
QMessageBox::warning(this, tr("View Textures"),
|
||||
tr("No active Elements Hunting preview scope is available."));
|
||||
return;
|
||||
}
|
||||
|
||||
TextureHashBrowserConfig browser_config;
|
||||
browser_config.title = tr("Textures for Current Element Selection");
|
||||
browser_config.empty_info_text =
|
||||
tr("No textures were captured yet for the current preview scope.");
|
||||
browser_config.current_label = tr("current preview scope");
|
||||
browser_config.fetch_current_label = []() {
|
||||
const auto current_status = ElementsGroupManager::GetInstance().GetStatus();
|
||||
return current_status.selected_match_filter_count > 0 ?
|
||||
(current_status.selected_match_filter_excluded ? QObject::tr("unchecked matches only") :
|
||||
QObject::tr("checked matches only")) :
|
||||
QObject::tr("whole current group");
|
||||
};
|
||||
browser_config.initial_selected_hashes = m_pending_texture_hashes;
|
||||
browser_config.fetch_current_entries = []() {
|
||||
return CollectTextureBrowserEntries(
|
||||
ElementsGroupManager::GetInstance().GetCurrentTextureSourceDraws());
|
||||
};
|
||||
browser_config.apply_selected_hashes = [this](const std::vector<u64>& hashes) {
|
||||
m_pending_texture_hashes = hashes;
|
||||
RefreshPendingTextureSummary();
|
||||
};
|
||||
|
||||
ShowTextureHashBrowserDialog(this, browser_config);
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::SaveCurrentOverride()
|
||||
{
|
||||
if (m_game_id.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Hunting"), tr("No game is currently running."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto& manager = ElementsGroupManager::GetInstance();
|
||||
const auto status = manager.GetStatus();
|
||||
if (!status.highlighted_draw)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Hunting"),
|
||||
tr("No highlighted match is available in the current frame."));
|
||||
return;
|
||||
}
|
||||
if (!status.seed_valid)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Elements Hunting"), tr("Seed a draw first."));
|
||||
return;
|
||||
}
|
||||
|
||||
ElementsGroupManager::ElementGroupOverride initial;
|
||||
initial.name =
|
||||
fmt::format("Element Draw {}", status.highlighted_draw->draw_index >= 0 ?
|
||||
status.highlighted_draw->draw_index + 1 :
|
||||
0);
|
||||
initial.handling = ShaderHunter::HandlingType::Skip;
|
||||
initial.runtime_element = status.seed_signature;
|
||||
initial.texture_hashes = m_pending_texture_hashes;
|
||||
initial.texture_hashes_excluded =
|
||||
!m_pending_texture_hashes.empty() && m_texture_mode_combo->currentData().toBool();
|
||||
initial.selected_match_filter = manager.GetSelectedMatchFilters();
|
||||
initial.selected_match_filter_excluded = manager.GetSelectedMatchFilterExcluded();
|
||||
initial.refinement_enabled = false;
|
||||
|
||||
ElementsGroupOverrideAddEditDialog dialog(this, &initial, CollectAvailableFlags(m_game_id));
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
auto overrides = ElementsGroupManager::LoadOverridesFromINI(m_game_id);
|
||||
overrides.push_back(dialog.GetResult());
|
||||
ElementsGroupManager::SaveOverridesToINI(m_game_id, overrides);
|
||||
manager.LoadOverrides(m_game_id);
|
||||
emit OverridesChanged();
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::OnCurrentMatchSelectionChanged()
|
||||
{
|
||||
const auto items = m_current_match_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
|
||||
ElementsGroupManager::GetInstance().SelectMatch(items[0]->data(Qt::UserRole).toInt());
|
||||
RequestRefresh();
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::OnCurrentMatchItemChanged(QListWidgetItem* item)
|
||||
{
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
ElementsGroupManager::GetInstance().SetCurrentMatchFilterEnabled(
|
||||
item->data(Qt::UserRole).toInt(), item->checkState() == Qt::Checked);
|
||||
RequestRefresh();
|
||||
}
|
||||
|
||||
void ElementsHuntingDialog::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
auto& manager = ElementsGroupManager::GetInstance();
|
||||
manager.SetHuntEnabled(false);
|
||||
manager.SetPopupOpen(false);
|
||||
QDialog::closeEvent(event);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QPushButton;
|
||||
class QTimer;
|
||||
|
||||
class ElementsHuntingDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ElementsHuntingDialog(std::string game_id, QWidget* parent = nullptr);
|
||||
~ElementsHuntingDialog() override;
|
||||
|
||||
signals:
|
||||
void OverridesChanged();
|
||||
|
||||
private:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void CreateWidgets();
|
||||
void ConnectWidgets();
|
||||
void UpdateDisplay();
|
||||
void RequestRefresh();
|
||||
void RefreshPendingTextureSummary();
|
||||
void SaveCurrentOverride();
|
||||
void OnSeedSelectionChanged();
|
||||
void OnCurrentMatchSelectionChanged();
|
||||
void OnCurrentMatchItemChanged(QListWidgetItem* item);
|
||||
void ShowTextureBrowser();
|
||||
|
||||
std::string m_game_id;
|
||||
QCheckBox* m_enable_check = nullptr;
|
||||
QComboBox* m_option_combo = nullptr;
|
||||
QComboBox* m_match_filter_mode_combo = nullptr;
|
||||
QComboBox* m_texture_mode_combo = nullptr;
|
||||
QListWidget* m_seed_list = nullptr;
|
||||
QLabel* m_seed_label = nullptr;
|
||||
QLabel* m_match_label = nullptr;
|
||||
QLabel* m_pending_texture_label = nullptr;
|
||||
QCheckBox* m_auto_refresh_check = nullptr;
|
||||
QCheckBox* m_projection_check = nullptr;
|
||||
QCheckBox* m_layer_check = nullptr;
|
||||
QCheckBox* m_viewport_check = nullptr;
|
||||
QCheckBox* m_scissor_check = nullptr;
|
||||
QCheckBox* m_render_state_check = nullptr;
|
||||
QPushButton* m_refresh_button = nullptr;
|
||||
QPushButton* m_prev_match_button = nullptr;
|
||||
QPushButton* m_next_match_button = nullptr;
|
||||
QPushButton* m_view_textures_button = nullptr;
|
||||
QListWidget* m_current_match_list = nullptr;
|
||||
QPushButton* m_save_button = nullptr;
|
||||
QTimer* m_update_timer = nullptr;
|
||||
bool m_refresh_pending = true;
|
||||
std::vector<u64> m_pending_texture_hashes;
|
||||
};
|
||||
@@ -46,6 +46,8 @@ EnhancementsWidget::EnhancementsWidget(GraphicsPane* gfx_pane)
|
||||
connect(gfx_pane, &GraphicsPane::UseGPUTextureDecodingChanged, this, [this] {
|
||||
m_arbitrary_mipmap_detection->setEnabled(!ReadSetting(Config::GFX_ENABLE_GPU_TEXTURE_DECODING));
|
||||
});
|
||||
connect(&Settings::Instance(), &Settings::ConfigChanged, this,
|
||||
&EnhancementsWidget::UpdateStereoscopyAvailability);
|
||||
}
|
||||
|
||||
constexpr int ANISO_1x = std::to_underlying(AnisotropicFilteringMode::Force1x);
|
||||
@@ -208,9 +210,9 @@ void EnhancementsWidget::CreateWidgets()
|
||||
++row;
|
||||
|
||||
// Stereoscopy
|
||||
auto* stereoscopy_box = new QGroupBox(tr("Stereoscopy"));
|
||||
m_stereoscopy_box = new QGroupBox(tr("Stereoscopy"));
|
||||
auto* stereoscopy_layout = new QGridLayout();
|
||||
stereoscopy_box->setLayout(stereoscopy_layout);
|
||||
m_stereoscopy_box->setLayout(stereoscopy_layout);
|
||||
|
||||
m_3d_mode = new ConfigChoice({tr("Off"), tr("Side-by-Side"), tr("Top-and-Bottom"), tr("Anaglyph"),
|
||||
tr("HDMI 3D"), tr("Passive")},
|
||||
@@ -246,7 +248,7 @@ void EnhancementsWidget::CreateWidgets()
|
||||
m_3d_per_eye_resolution->hide();
|
||||
|
||||
main_layout->addWidget(enhancements_box);
|
||||
main_layout->addWidget(stereoscopy_box);
|
||||
main_layout->addWidget(m_stereoscopy_box);
|
||||
main_layout->addStretch();
|
||||
|
||||
setLayout(main_layout);
|
||||
@@ -351,12 +353,7 @@ void EnhancementsWidget::OnBackendChanged()
|
||||
m_configure_color_correction->setEnabled(g_backend_info.bSupportsPostProcessing);
|
||||
m_hdr->setEnabled(g_backend_info.bSupportsHDROutput);
|
||||
|
||||
// Stereoscopy
|
||||
const bool supports_stereoscopy = g_backend_info.bSupportsGeometryShaders;
|
||||
m_3d_mode->setEnabled(supports_stereoscopy);
|
||||
m_3d_convergence->setEnabled(supports_stereoscopy);
|
||||
m_3d_depth->setEnabled(supports_stereoscopy);
|
||||
m_3d_swap_eyes->setEnabled(supports_stereoscopy);
|
||||
UpdateStereoscopyAvailability();
|
||||
|
||||
// PostProcessing
|
||||
const bool supports_postprocessing = g_backend_info.bSupportsPostProcessing;
|
||||
@@ -378,6 +375,13 @@ void EnhancementsWidget::OnBackendChanged()
|
||||
UpdateAntialiasingOptions();
|
||||
}
|
||||
|
||||
void EnhancementsWidget::UpdateStereoscopyAvailability()
|
||||
{
|
||||
const bool supports_stereoscopy = g_backend_info.bSupportsGeometryShaders;
|
||||
const bool openxr_enabled = Config::Get(Config::GFX_VR_ENABLE_OPENXR);
|
||||
m_stereoscopy_box->setEnabled(supports_stereoscopy && !openxr_enabled);
|
||||
}
|
||||
|
||||
void EnhancementsWidget::ShaderChanged()
|
||||
{
|
||||
auto shader = ReadSetting(Config::GFX_ENHANCE_POST_SHADER);
|
||||
@@ -412,6 +416,7 @@ void EnhancementsWidget::OnConfigChanged()
|
||||
// being global.
|
||||
m_texture_filtering_combo->setEnabled(ReadSetting(Config::GFX_HACK_FAST_TEXTURE_SAMPLING));
|
||||
m_arbitrary_mipmap_detection->setEnabled(!ReadSetting(Config::GFX_ENABLE_GPU_TEXTURE_DECODING));
|
||||
UpdateStereoscopyAvailability();
|
||||
UpdateAntialiasingOptions();
|
||||
|
||||
// Needs to update after deleting a key for 3d settings.
|
||||
@@ -539,7 +544,8 @@ void EnhancementsWidget::AddDescriptions()
|
||||
"emulation speed and sometimes causes issues.<br><br>Side-by-Side and Top-and-Bottom are "
|
||||
"used by most 3D TVs.<br>Anaglyph is used for Red-Cyan colored glasses.<br>HDMI 3D is "
|
||||
"used when the monitor supports 3D display resolutions.<br>Passive is another type of 3D "
|
||||
"used by some TVs.<br><br><dolphin_emphasis>If unsure, select Off.</dolphin_emphasis>");
|
||||
"used by some TVs."
|
||||
"<br><br><dolphin_emphasis>If unsure, select Off.</dolphin_emphasis>");
|
||||
static const char TR_3D_DEPTH_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Controls the separation distance between the virtual cameras.<br><br>A higher "
|
||||
"value creates a stronger feeling of depth while a lower value is more comfortable.");
|
||||
|
||||
@@ -11,6 +11,7 @@ class ConfigComplexChoice;
|
||||
class ConfigStringChoice;
|
||||
class ConfigFloatSlider;
|
||||
class GraphicsPane;
|
||||
class QGroupBox;
|
||||
class QPushButton;
|
||||
class QLabel;
|
||||
class ToolTipPushButton;
|
||||
@@ -37,6 +38,7 @@ private:
|
||||
void AddDescriptions();
|
||||
|
||||
void OnBackendChanged();
|
||||
void UpdateStereoscopyAvailability();
|
||||
void UpdateAntialiasingOptions();
|
||||
void LoadPostProcessingShaders();
|
||||
void ShaderChanged();
|
||||
@@ -63,6 +65,7 @@ private:
|
||||
ConfigBool* m_hdr;
|
||||
|
||||
// Stereoscopy
|
||||
QGroupBox* m_stereoscopy_box;
|
||||
ConfigChoice* m_3d_mode;
|
||||
ConfigFloatSlider* m_3d_depth;
|
||||
QLabel* m_3d_depth_value;
|
||||
|
||||
@@ -32,6 +32,19 @@
|
||||
#include "VideoCommon/VideoBackendBase.h"
|
||||
#include "VideoCommon/VideoConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
QString GetBackendDisplayNameForUI(std::string_view backend_name)
|
||||
{
|
||||
if (backend_name == "Direct3D 11")
|
||||
return QObject::tr("Direct3D 11 (VR)");
|
||||
if (backend_name == "Vulkan")
|
||||
return QObject::tr("Vulkan (VR)");
|
||||
|
||||
return QObject::tr(backend_name.data());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GeneralWidget::GeneralWidget(GraphicsPane* gfx_pane) : m_game_layer{gfx_pane->GetConfigLayer()}
|
||||
{
|
||||
CreateWidgets();
|
||||
@@ -56,7 +69,7 @@ void GeneralWidget::CreateWidgets()
|
||||
std::vector<std::pair<QString, QString>> options;
|
||||
for (auto& backend : VideoBackendBase::GetAvailableBackends())
|
||||
{
|
||||
options.push_back(std::make_pair(tr(backend->GetDisplayName().c_str()),
|
||||
options.push_back(std::make_pair(GetBackendDisplayNameForUI(backend->GetDisplayName()),
|
||||
QString::fromStdString(backend->GetConfigName())));
|
||||
}
|
||||
m_backend_combo = new ConfigStringChoice(options, Config::MAIN_GFX_BACKEND, m_game_layer);
|
||||
@@ -292,7 +305,7 @@ void GeneralWidget::AddDescriptions()
|
||||
m_backend_combo->SetTitle(tr("Backend"));
|
||||
m_backend_combo->SetDescription(
|
||||
tr(TR_BACKEND_DESCRIPTION)
|
||||
.arg(QString::fromStdString(VideoBackendBase::GetDefaultBackendDisplayName())));
|
||||
.arg(GetBackendDisplayNameForUI(VideoBackendBase::GetDefaultBackendDisplayName())));
|
||||
|
||||
m_adapter_combo->SetTitle(tr("Adapter"));
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "DolphinQt/Config/ConfigControls/ConfigSlider.h"
|
||||
#include "DolphinQt/Config/GameConfigWidget.h"
|
||||
#include "DolphinQt/Config/Graphics/GraphicsPane.h"
|
||||
#include "DolphinQt/Settings.h"
|
||||
|
||||
#include "VideoCommon/VideoConfig.h"
|
||||
|
||||
@@ -26,6 +27,28 @@ HacksWidget::HacksWidget(GraphicsPane* gfx_pane) : m_game_layer{gfx_pane->GetCon
|
||||
AddDescriptions();
|
||||
|
||||
connect(gfx_pane, &GraphicsPane::BackendChanged, this, &HacksWidget::OnBackendChanged);
|
||||
const auto update_openxr_immediate_xfb = [this] {
|
||||
// When VR Lock Head Pose is active, Immediately Present XFB must stay off.
|
||||
const bool vr_lock = Config::Get(Config::GFX_VR_ENABLE_OPENXR) &&
|
||||
Config::Get(Config::GFX_VR_LOCK_HEAD_POSE);
|
||||
// When VR Force Immediately Present XFB is active, force it on and grey out.
|
||||
const bool vr_force = Config::Get(Config::GFX_VR_ENABLE_OPENXR) &&
|
||||
Config::Get(Config::GFX_VR_AUTO_IMMEDIATE_XFB);
|
||||
|
||||
m_immediate_xfb->setDisabled(vr_lock || vr_force);
|
||||
if (vr_lock)
|
||||
{
|
||||
Config::SetBaseOrCurrent(Config::GFX_HACK_IMMEDIATE_XFB, false);
|
||||
}
|
||||
else if (vr_force && !Config::Get(Config::GFX_HACK_IMMEDIATE_XFB))
|
||||
{
|
||||
Config::SetBaseOrCurrent(Config::GFX_HACK_IMMEDIATE_XFB, true);
|
||||
}
|
||||
|
||||
UpdateSkipPresentingDuplicateFramesEnabled();
|
||||
};
|
||||
connect(&Settings::Instance(), &Settings::ConfigChanged, this, update_openxr_immediate_xfb);
|
||||
update_openxr_immediate_xfb();
|
||||
OnBackendChanged(QString::fromStdString(Config::Get(Config::MAIN_GFX_BACKEND)));
|
||||
connect(m_gpu_texture_decoding, &QCheckBox::toggled,
|
||||
[gfx_pane] { emit gfx_pane->UseGPUTextureDecodingChanged(); });
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/HideObjectAddEditDialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "VideoCommon/HideObjectEngine.h"
|
||||
|
||||
HideObjectAddEditDialog::HideObjectAddEditDialog(
|
||||
QWidget* parent, const HideObjectEngine::HideObject* existing_code,
|
||||
const std::vector<HideObjectEngine::HideObject>& all_codes)
|
||||
: QDialog(parent), m_all_codes(all_codes)
|
||||
{
|
||||
if (existing_code)
|
||||
{
|
||||
m_is_edit = true;
|
||||
m_original_name = existing_code->name;
|
||||
m_result = *existing_code;
|
||||
if (!m_result.entries.empty())
|
||||
m_current_entry = m_result.entries[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
m_current_entry.type = HideObjectEngine::HideObjectType::Bits16;
|
||||
m_current_entry.value_upper = 0;
|
||||
m_current_entry.value_lower = 0;
|
||||
}
|
||||
|
||||
setWindowTitle(m_is_edit ? tr("Edit Hide Object Code") : tr("Add Hide Object Code"));
|
||||
setMinimumWidth(400);
|
||||
|
||||
CreateWidgets();
|
||||
ConnectWidgets();
|
||||
UpdateValueDisplay();
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::CreateWidgets()
|
||||
{
|
||||
auto* name_label = new QLabel(tr("Name:"));
|
||||
m_name_edit = new QLineEdit;
|
||||
if (m_is_edit)
|
||||
m_name_edit->setText(QString::fromStdString(m_result.name));
|
||||
else
|
||||
m_name_edit->setPlaceholderText(tr("Enter code name..."));
|
||||
|
||||
auto* type_label = new QLabel(tr("Size:"));
|
||||
m_type_combo = new QComboBox;
|
||||
for (int i = 0; i < static_cast<int>(HideObjectEngine::HideObjectType::Count); i++)
|
||||
m_type_combo->addItem(QString::fromLatin1(
|
||||
HideObjectEngine::GetTypeName(static_cast<HideObjectEngine::HideObjectType>(i))));
|
||||
m_type_combo->setCurrentIndex(static_cast<int>(m_current_entry.type));
|
||||
|
||||
auto* value_label = new QLabel(tr("Value (hex):"));
|
||||
m_value_edit = new QLineEdit;
|
||||
m_value_edit->setFont(QFont(QStringLiteral("Courier New"), 10));
|
||||
|
||||
m_up_button = new QPushButton(tr("Up"));
|
||||
m_down_button = new QPushButton(tr("Down"));
|
||||
m_range_finder_toggle = new QCheckBox(tr("Range Finder"));
|
||||
m_range_label = new QLabel;
|
||||
m_range_lower_slider = new QSlider(Qt::Horizontal);
|
||||
m_range_upper_slider = new QSlider(Qt::Horizontal);
|
||||
|
||||
m_range_lower_slider->setRange(0, 255);
|
||||
m_range_upper_slider->setRange(0, 255);
|
||||
m_range_lower_slider->setValue(0);
|
||||
m_range_upper_slider->setValue(255);
|
||||
UpdateRangeLabel();
|
||||
|
||||
const QString tooltip =
|
||||
tr("The Up/Down buttons can be used to find new codes.\n"
|
||||
"While the game is playing, find an object you want to hide and select '8bits'.\n"
|
||||
"Keep clicking Up until the object disappears.\n"
|
||||
"Now choose '16bits' and continue clicking Up until the object is hidden again.\n"
|
||||
"Repeat until the code is long enough to be unique.\n"
|
||||
"Warning: Too short of a code may hide other objects too.");
|
||||
m_up_button->setToolTip(tooltip);
|
||||
m_down_button->setToolTip(tooltip);
|
||||
|
||||
auto* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
|
||||
auto* grid = new QGridLayout;
|
||||
grid->addWidget(name_label, 0, 0);
|
||||
grid->addWidget(m_name_edit, 0, 1, 1, 3);
|
||||
grid->addWidget(type_label, 1, 0);
|
||||
grid->addWidget(m_type_combo, 1, 1, 1, 3);
|
||||
grid->addWidget(value_label, 2, 0);
|
||||
grid->addWidget(m_value_edit, 2, 1);
|
||||
grid->addWidget(m_up_button, 2, 2);
|
||||
grid->addWidget(m_down_button, 2, 3);
|
||||
grid->addWidget(m_range_finder_toggle, 3, 0, 1, 4);
|
||||
grid->addWidget(m_range_label, 4, 0, 1, 4);
|
||||
grid->addWidget(new QLabel(tr("Lower Bound:")), 5, 0);
|
||||
grid->addWidget(m_range_lower_slider, 5, 1, 1, 3);
|
||||
grid->addWidget(new QLabel(tr("Upper Bound:")), 6, 0);
|
||||
grid->addWidget(m_range_upper_slider, 6, 1, 1, 3);
|
||||
|
||||
m_range_label->setEnabled(false);
|
||||
m_range_lower_slider->setEnabled(false);
|
||||
m_range_upper_slider->setEnabled(false);
|
||||
|
||||
auto* layout = new QVBoxLayout{this};
|
||||
layout->addLayout(grid);
|
||||
layout->addWidget(button_box);
|
||||
|
||||
connect(button_box, &QDialogButtonBox::accepted, this, &HideObjectAddEditDialog::OnAccept);
|
||||
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::ConnectWidgets()
|
||||
{
|
||||
connect(m_type_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&HideObjectAddEditDialog::OnTypeChanged);
|
||||
connect(m_up_button, &QPushButton::clicked, this, &HideObjectAddEditDialog::OnUpClicked);
|
||||
connect(m_down_button, &QPushButton::clicked, this, &HideObjectAddEditDialog::OnDownClicked);
|
||||
connect(m_range_finder_toggle, &QCheckBox::toggled, this,
|
||||
&HideObjectAddEditDialog::OnRangeFinderToggled);
|
||||
connect(m_range_lower_slider, &QSlider::valueChanged, this,
|
||||
&HideObjectAddEditDialog::OnRangeBoundsChanged);
|
||||
connect(m_range_upper_slider, &QSlider::valueChanged, this,
|
||||
&HideObjectAddEditDialog::OnRangeBoundsChanged);
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::UpdateValueDisplay()
|
||||
{
|
||||
const int char_len = HideObjectEngine::GetByteCount(m_current_entry.type) * 2;
|
||||
|
||||
if (char_len <= 16)
|
||||
{
|
||||
// Fits in value_lower
|
||||
m_value_edit->setText(
|
||||
QStringLiteral("%1").arg(m_current_entry.value_lower, char_len, 16, QLatin1Char('0')).toUpper());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need both upper and lower
|
||||
const int upper_chars = char_len - 16;
|
||||
m_value_edit->setText(
|
||||
QStringLiteral("%1%2")
|
||||
.arg(m_current_entry.value_upper, upper_chars, 16, QLatin1Char('0'))
|
||||
.arg(m_current_entry.value_lower, 16, 16, QLatin1Char('0'))
|
||||
.toUpper());
|
||||
}
|
||||
}
|
||||
|
||||
bool HideObjectAddEditDialog::ParseValueFromUI()
|
||||
{
|
||||
const QString text = m_value_edit->text().trimmed();
|
||||
if (text.isEmpty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Error"), tr("Value cannot be empty."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const int expected_chars = HideObjectEngine::GetByteCount(m_current_entry.type) * 2;
|
||||
|
||||
bool ok = false;
|
||||
if (expected_chars <= 16)
|
||||
{
|
||||
m_current_entry.value_lower = text.toULongLong(&ok, 16);
|
||||
m_current_entry.value_upper = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Split: upper portion + lower 16 hex chars
|
||||
if (text.length() > 16)
|
||||
{
|
||||
const QString upper_str = text.left(text.length() - 16);
|
||||
const QString lower_str = text.right(16);
|
||||
m_current_entry.value_upper = upper_str.toULongLong(&ok, 16);
|
||||
if (ok)
|
||||
m_current_entry.value_lower = lower_str.toULongLong(&ok, 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_current_entry.value_upper = 0;
|
||||
m_current_entry.value_lower = text.toULongLong(&ok, 16);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Error"),
|
||||
tr("Invalid hex value. Use characters 0-9 and A-F only."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::OnTypeChanged()
|
||||
{
|
||||
if (!ParseValueFromUI())
|
||||
return;
|
||||
|
||||
const int old_byte_count = HideObjectEngine::GetByteCount(m_current_entry.type);
|
||||
m_current_entry.type = static_cast<HideObjectEngine::HideObjectType>(m_type_combo->currentIndex());
|
||||
const int new_byte_count = HideObjectEngine::GetByteCount(m_current_entry.type);
|
||||
|
||||
auto shift_left_128 = [](u64& upper, u64& lower, int bits) {
|
||||
if (bits <= 0)
|
||||
return;
|
||||
if (bits >= 128)
|
||||
{
|
||||
upper = 0;
|
||||
lower = 0;
|
||||
return;
|
||||
}
|
||||
if (bits >= 64)
|
||||
{
|
||||
upper = lower << (bits - 64);
|
||||
lower = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
upper = (upper << bits) | (lower >> (64 - bits));
|
||||
lower <<= bits;
|
||||
};
|
||||
|
||||
auto shift_right_128 = [](u64& upper, u64& lower, int bits) {
|
||||
if (bits <= 0)
|
||||
return;
|
||||
if (bits >= 128)
|
||||
{
|
||||
upper = 0;
|
||||
lower = 0;
|
||||
return;
|
||||
}
|
||||
if (bits >= 64)
|
||||
{
|
||||
lower = upper >> (bits - 64);
|
||||
upper = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
lower = (lower >> bits) | (upper << (64 - bits));
|
||||
upper >>= bits;
|
||||
};
|
||||
|
||||
// Keep current bytes in place relative to the most-significant side when size changes.
|
||||
// Example: 8-bit 01 -> 16-bit 0100; and 16-bit 0100 -> 8-bit 01.
|
||||
const int shift_bytes = new_byte_count - old_byte_count;
|
||||
if (shift_bytes > 0)
|
||||
shift_left_128(m_current_entry.value_upper, m_current_entry.value_lower, shift_bytes * 8);
|
||||
else if (shift_bytes < 0)
|
||||
shift_right_128(m_current_entry.value_upper, m_current_entry.value_lower, -shift_bytes * 8);
|
||||
|
||||
// Truncate to the newly selected size.
|
||||
if (new_byte_count <= 8)
|
||||
{
|
||||
m_current_entry.value_upper = 0;
|
||||
if (new_byte_count < 8)
|
||||
{
|
||||
const u64 mask = (1ULL << (new_byte_count * 8)) - 1;
|
||||
m_current_entry.value_lower &= mask;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const int upper_bytes = new_byte_count - 8;
|
||||
if (upper_bytes < 8)
|
||||
{
|
||||
const u64 mask = (1ULL << (upper_bytes * 8)) - 1;
|
||||
m_current_entry.value_upper &= mask;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateValueDisplay();
|
||||
ApplyTemporarily();
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::OnUpClicked()
|
||||
{
|
||||
if (!ParseValueFromUI())
|
||||
return;
|
||||
|
||||
// Brute-force only the last byte (two hex chars), without carrying into higher bytes.
|
||||
const u8 low_byte = static_cast<u8>(m_current_entry.value_lower & 0xFFULL);
|
||||
const u8 next_low_byte = static_cast<u8>(low_byte + 1);
|
||||
m_current_entry.value_lower = (m_current_entry.value_lower & ~0xFFULL) | next_low_byte;
|
||||
|
||||
UpdateValueDisplay();
|
||||
ApplyTemporarily();
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::OnDownClicked()
|
||||
{
|
||||
if (!ParseValueFromUI())
|
||||
return;
|
||||
|
||||
// Brute-force only the last byte (two hex chars), without borrowing from higher bytes.
|
||||
const u8 low_byte = static_cast<u8>(m_current_entry.value_lower & 0xFFULL);
|
||||
const u8 prev_low_byte = static_cast<u8>(low_byte - 1);
|
||||
m_current_entry.value_lower = (m_current_entry.value_lower & ~0xFFULL) | prev_low_byte;
|
||||
|
||||
UpdateValueDisplay();
|
||||
ApplyTemporarily();
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::OnRangeFinderToggled(bool enabled)
|
||||
{
|
||||
m_range_label->setEnabled(enabled);
|
||||
m_range_lower_slider->setEnabled(enabled);
|
||||
m_range_upper_slider->setEnabled(enabled);
|
||||
UpdateRangeLabel();
|
||||
ApplyTemporarily();
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::OnRangeBoundsChanged()
|
||||
{
|
||||
if (sender() == m_range_lower_slider && m_range_lower_slider->value() > m_range_upper_slider->value())
|
||||
m_range_upper_slider->setValue(m_range_lower_slider->value());
|
||||
else if (sender() == m_range_upper_slider &&
|
||||
m_range_upper_slider->value() < m_range_lower_slider->value())
|
||||
m_range_lower_slider->setValue(m_range_upper_slider->value());
|
||||
|
||||
UpdateRangeLabel();
|
||||
if (!m_range_finder_toggle->isChecked())
|
||||
return;
|
||||
|
||||
if (!ParseValueFromUI())
|
||||
return;
|
||||
|
||||
ApplyTemporarily();
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::UpdateRangeLabel()
|
||||
{
|
||||
const int lower = std::min(m_range_lower_slider->value(), m_range_upper_slider->value());
|
||||
const int upper = std::max(m_range_lower_slider->value(), m_range_upper_slider->value());
|
||||
m_range_label->setText(
|
||||
tr("Current range: %1 - %2")
|
||||
.arg(QStringLiteral("%1").arg(lower, 2, 16, QLatin1Char('0')).toUpper())
|
||||
.arg(QStringLiteral("%1").arg(upper, 2, 16, QLatin1Char('0')).toUpper()));
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::ApplyTemporarily()
|
||||
{
|
||||
// Temporarily apply this single code to the running game for immediate visual feedback.
|
||||
std::vector<HideObjectEngine::HideObject> temp_list;
|
||||
|
||||
if (m_range_finder_toggle->isChecked())
|
||||
{
|
||||
// Range finder: apply all values for the last byte in [lower..upper], fixed upper bytes.
|
||||
const int lower = std::min(m_range_lower_slider->value(), m_range_upper_slider->value());
|
||||
const int upper = std::max(m_range_lower_slider->value(), m_range_upper_slider->value());
|
||||
temp_list.reserve(static_cast<size_t>(upper - lower + 1));
|
||||
|
||||
for (int v = lower; v <= upper; ++v)
|
||||
{
|
||||
HideObjectEngine::HideObject temp_code;
|
||||
temp_code.name = "temp_range";
|
||||
temp_code.active = true;
|
||||
temp_code.user_defined = false;
|
||||
|
||||
HideObjectEngine::HideObjectEntry entry = m_current_entry;
|
||||
entry.value_lower = (entry.value_lower & ~0xFFULL) | static_cast<u64>(v & 0xFF);
|
||||
temp_code.entries.push_back(entry);
|
||||
|
||||
temp_list.push_back(std::move(temp_code));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single-value brute force: only the current entry.
|
||||
HideObjectEngine::HideObject temp_code;
|
||||
temp_code.name = "temp_brute_force";
|
||||
temp_code.entries.push_back(m_current_entry);
|
||||
temp_code.active = true;
|
||||
temp_code.user_defined = false;
|
||||
temp_list.push_back(std::move(temp_code));
|
||||
}
|
||||
|
||||
HideObjectEngine::Engine::GetInstance().ApplyCodes(temp_list);
|
||||
}
|
||||
|
||||
void HideObjectAddEditDialog::OnAccept()
|
||||
{
|
||||
const QString name = m_name_edit->text().trimmed();
|
||||
if (name.isEmpty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Error"), tr("Please enter a name for this code."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check name uniqueness
|
||||
for (const auto& code : m_all_codes)
|
||||
{
|
||||
if (code.name == name.toStdString() && code.name != m_original_name)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Error"),
|
||||
tr("Name is already in use. Please choose a unique name."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParseValueFromUI())
|
||||
return;
|
||||
|
||||
m_result.name = name.toStdString();
|
||||
m_result.entries.clear();
|
||||
m_result.entries.push_back(m_current_entry);
|
||||
m_result.active = true;
|
||||
m_result.user_defined = true;
|
||||
|
||||
accept();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "VideoCommon/HideObjectEngine.h"
|
||||
|
||||
class QComboBox;
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QSlider;
|
||||
|
||||
class HideObjectAddEditDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Pass nullptr for new code, or pointer to existing code for editing.
|
||||
// parent_codes is the full list (for name uniqueness check).
|
||||
explicit HideObjectAddEditDialog(QWidget* parent,
|
||||
const HideObjectEngine::HideObject* existing_code,
|
||||
const std::vector<HideObjectEngine::HideObject>& all_codes);
|
||||
|
||||
HideObjectEngine::HideObject GetResult() const { return m_result; }
|
||||
|
||||
private:
|
||||
void CreateWidgets();
|
||||
void ConnectWidgets();
|
||||
|
||||
void OnTypeChanged();
|
||||
void OnUpClicked();
|
||||
void OnDownClicked();
|
||||
void OnRangeFinderToggled(bool enabled);
|
||||
void OnRangeBoundsChanged();
|
||||
void OnAccept();
|
||||
|
||||
void UpdateValueDisplay();
|
||||
void UpdateRangeLabel();
|
||||
bool ParseValueFromUI();
|
||||
void ApplyTemporarily();
|
||||
|
||||
bool m_is_edit = false;
|
||||
std::string m_original_name; // For edit mode: original name to exclude from uniqueness check
|
||||
|
||||
HideObjectEngine::HideObject m_result;
|
||||
HideObjectEngine::HideObjectEntry m_current_entry;
|
||||
|
||||
const std::vector<HideObjectEngine::HideObject>& m_all_codes;
|
||||
|
||||
QLineEdit* m_name_edit;
|
||||
QComboBox* m_type_combo;
|
||||
QLineEdit* m_value_edit;
|
||||
QPushButton* m_up_button;
|
||||
QPushButton* m_down_button;
|
||||
QCheckBox* m_range_finder_toggle;
|
||||
QLabel* m_range_label;
|
||||
QSlider* m_range_lower_slider;
|
||||
QSlider* m_range_upper_slider;
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/HideObjectWidget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "DolphinQt/Config/HideObjectAddEditDialog.h"
|
||||
#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
|
||||
#include "VideoCommon/HideObjectEngine.h"
|
||||
|
||||
HideObjectWidget::HideObjectWidget(std::string game_id) : m_game_id(std::move(game_id))
|
||||
{
|
||||
CreateWidgets();
|
||||
ConnectWidgets();
|
||||
LoadCodes();
|
||||
}
|
||||
|
||||
HideObjectWidget::~HideObjectWidget() = default;
|
||||
|
||||
void HideObjectWidget::CreateWidgets()
|
||||
{
|
||||
m_code_list = new QListWidget;
|
||||
|
||||
m_code_add = new NonDefaultQPushButton(tr("&Add..."));
|
||||
m_code_edit = new NonDefaultQPushButton(tr("&Edit..."));
|
||||
m_code_remove = new NonDefaultQPushButton(tr("&Remove"));
|
||||
m_code_refresh = new NonDefaultQPushButton(tr("Re&fresh List"));
|
||||
m_code_reload = new NonDefaultQPushButton(tr("Reload &Codes"));
|
||||
|
||||
m_code_edit->setEnabled(false);
|
||||
m_code_remove->setEnabled(false);
|
||||
|
||||
auto* info_label = new QLabel(
|
||||
tr("Hide object codes suppress rendering of specific vertex data patterns.\n"
|
||||
"Use Add to create a new code, or use the Up/Down buttons in the editor\n"
|
||||
"to discover codes while the game is running."));
|
||||
info_label->setWordWrap(true);
|
||||
|
||||
auto* button_layout = new QHBoxLayout;
|
||||
button_layout->addWidget(m_code_add);
|
||||
button_layout->addWidget(m_code_edit);
|
||||
button_layout->addWidget(m_code_remove);
|
||||
button_layout->addStretch();
|
||||
button_layout->addWidget(m_code_refresh);
|
||||
button_layout->addWidget(m_code_reload);
|
||||
|
||||
auto* layout = new QVBoxLayout{this};
|
||||
layout->addWidget(info_label);
|
||||
layout->addWidget(m_code_list);
|
||||
layout->addLayout(button_layout);
|
||||
}
|
||||
|
||||
void HideObjectWidget::ConnectWidgets()
|
||||
{
|
||||
connect(m_code_list, &QListWidget::itemChanged, this, &HideObjectWidget::OnItemChanged);
|
||||
connect(m_code_list, &QListWidget::itemSelectionChanged, this,
|
||||
&HideObjectWidget::OnSelectionChanged);
|
||||
connect(m_code_add, &QPushButton::clicked, this, &HideObjectWidget::OnAddClicked);
|
||||
connect(m_code_edit, &QPushButton::clicked, this, &HideObjectWidget::OnEditClicked);
|
||||
connect(m_code_remove, &QPushButton::clicked, this, &HideObjectWidget::OnRemoveClicked);
|
||||
connect(m_code_refresh, &QPushButton::clicked, this, &HideObjectWidget::OnRefreshClicked);
|
||||
connect(m_code_reload, &QPushButton::clicked, this, &HideObjectWidget::OnReloadClicked);
|
||||
}
|
||||
|
||||
void HideObjectWidget::LoadCodes()
|
||||
{
|
||||
m_codes = HideObjectEngine::LoadFromINI(m_game_id);
|
||||
|
||||
m_code_list->setEnabled(!m_game_id.empty());
|
||||
m_code_edit->setEnabled(false);
|
||||
m_code_remove->setEnabled(false);
|
||||
|
||||
UpdateList();
|
||||
}
|
||||
|
||||
void HideObjectWidget::SaveCodes()
|
||||
{
|
||||
HideObjectEngine::SaveToINI(m_game_id, m_codes);
|
||||
}
|
||||
|
||||
void HideObjectWidget::UpdateList()
|
||||
{
|
||||
const QSignalBlocker blocker(m_code_list);
|
||||
m_code_list->clear();
|
||||
|
||||
for (size_t i = 0; i < m_codes.size(); i++)
|
||||
{
|
||||
const auto& code = m_codes[i];
|
||||
|
||||
QString display;
|
||||
if (!code.entries.empty())
|
||||
{
|
||||
const auto& entry = code.entries[0];
|
||||
const int char_len = HideObjectEngine::GetByteCount(entry.type) * 2;
|
||||
QString value_str;
|
||||
if (char_len <= 16)
|
||||
{
|
||||
value_str = QStringLiteral("%1")
|
||||
.arg(entry.value_lower, char_len, 16, QLatin1Char('0'))
|
||||
.toUpper();
|
||||
}
|
||||
else
|
||||
{
|
||||
const int upper_chars = char_len - 16;
|
||||
value_str = QStringLiteral("%1%2")
|
||||
.arg(entry.value_upper, upper_chars, 16, QLatin1Char('0'))
|
||||
.arg(entry.value_lower, 16, 16, QLatin1Char('0'))
|
||||
.toUpper();
|
||||
}
|
||||
|
||||
display = QStringLiteral("%1 [%2 %3]")
|
||||
.arg(QString::fromStdString(code.name))
|
||||
.arg(QString::fromLatin1(HideObjectEngine::GetTypeName(entry.type)))
|
||||
.arg(value_str);
|
||||
}
|
||||
else
|
||||
{
|
||||
display = QString::fromStdString(code.name);
|
||||
}
|
||||
|
||||
auto* item = new QListWidgetItem(display);
|
||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(code.active ? Qt::Checked : Qt::Unchecked);
|
||||
item->setData(Qt::UserRole, static_cast<int>(i));
|
||||
|
||||
m_code_list->addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
void HideObjectWidget::OnItemChanged(QListWidgetItem* item)
|
||||
{
|
||||
const int row = m_code_list->row(item);
|
||||
if (row < 0 || row >= static_cast<int>(m_codes.size()))
|
||||
return;
|
||||
|
||||
m_codes[row].active = (item->checkState() == Qt::Checked);
|
||||
SaveCodes();
|
||||
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
|
||||
}
|
||||
|
||||
void HideObjectWidget::OnSelectionChanged()
|
||||
{
|
||||
const auto items = m_code_list->selectedItems();
|
||||
const bool has_selection = !items.empty();
|
||||
m_code_edit->setEnabled(has_selection);
|
||||
m_code_remove->setEnabled(has_selection);
|
||||
}
|
||||
|
||||
void HideObjectWidget::OnAddClicked()
|
||||
{
|
||||
HideObjectAddEditDialog dialog(this, nullptr, m_codes);
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
m_codes.push_back(dialog.GetResult());
|
||||
SaveCodes();
|
||||
UpdateList();
|
||||
|
||||
// Re-apply codes to runtime engine so the new code takes effect immediately
|
||||
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dialog was cancelled — restore the full code set to the runtime engine
|
||||
// (in case brute-force Up/Down applied temporary codes)
|
||||
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
|
||||
}
|
||||
}
|
||||
|
||||
void HideObjectWidget::OnEditClicked()
|
||||
{
|
||||
const auto items = m_code_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
|
||||
const int row = m_code_list->row(items[0]);
|
||||
if (row < 0 || row >= static_cast<int>(m_codes.size()))
|
||||
return;
|
||||
|
||||
HideObjectAddEditDialog dialog(this, &m_codes[row], m_codes);
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
m_codes[row] = dialog.GetResult();
|
||||
SaveCodes();
|
||||
UpdateList();
|
||||
|
||||
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
|
||||
}
|
||||
else
|
||||
{
|
||||
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
|
||||
}
|
||||
}
|
||||
|
||||
void HideObjectWidget::OnRemoveClicked()
|
||||
{
|
||||
const auto items = m_code_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
|
||||
const int row = m_code_list->row(items[0]);
|
||||
if (row < 0 || row >= static_cast<int>(m_codes.size()))
|
||||
return;
|
||||
|
||||
m_codes.erase(m_codes.begin() + row);
|
||||
|
||||
SaveCodes();
|
||||
UpdateList();
|
||||
HideObjectEngine::Engine::GetInstance().ApplyCodes(m_codes);
|
||||
|
||||
m_code_edit->setEnabled(false);
|
||||
m_code_remove->setEnabled(false);
|
||||
}
|
||||
|
||||
void HideObjectWidget::OnRefreshClicked()
|
||||
{
|
||||
LoadCodes();
|
||||
}
|
||||
|
||||
void HideObjectWidget::OnReloadClicked()
|
||||
{
|
||||
HideObjectEngine::Engine::GetInstance().LoadCodes(m_game_id);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "VideoCommon/HideObjectEngine.h"
|
||||
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QPushButton;
|
||||
|
||||
class HideObjectWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HideObjectWidget(std::string game_id);
|
||||
~HideObjectWidget() override;
|
||||
|
||||
private:
|
||||
void CreateWidgets();
|
||||
void ConnectWidgets();
|
||||
void UpdateList();
|
||||
void LoadCodes();
|
||||
void SaveCodes();
|
||||
|
||||
void OnItemChanged(QListWidgetItem* item);
|
||||
void OnSelectionChanged();
|
||||
void OnAddClicked();
|
||||
void OnEditClicked();
|
||||
void OnRemoveClicked();
|
||||
void OnRefreshClicked();
|
||||
void OnReloadClicked();
|
||||
|
||||
std::string m_game_id;
|
||||
|
||||
QListWidget* m_code_list;
|
||||
QPushButton* m_code_add;
|
||||
QPushButton* m_code_edit;
|
||||
QPushButton* m_code_remove;
|
||||
QPushButton* m_code_refresh;
|
||||
QPushButton* m_code_reload;
|
||||
|
||||
std::vector<HideObjectEngine::HideObject> m_codes;
|
||||
};
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/ImportShaderFindElementDialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QBrush>
|
||||
#include <QCheckBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QColor>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
QString FormatTextureSummary(const std::array<u64, 8>& textures)
|
||||
{
|
||||
QStringList parts;
|
||||
for (u64 texture_hash : textures)
|
||||
{
|
||||
if (texture_hash == 0)
|
||||
continue;
|
||||
parts << QStringLiteral("%1")
|
||||
.arg(static_cast<qulonglong>(texture_hash), 16, 16, QLatin1Char('0'));
|
||||
if (parts.size() >= 2)
|
||||
break;
|
||||
}
|
||||
return parts.isEmpty() ? QObject::tr("No textures") : parts.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
QString FormatProjectionLabel(const ShaderHunter::RuntimeElementSignature& sig)
|
||||
{
|
||||
if (sig.perspective)
|
||||
{
|
||||
return QObject::tr("Perspective %1x%2")
|
||||
.arg(sig.perspective_hfov_x100 / 100.0, 0, 'f', 2)
|
||||
.arg(sig.perspective_vfov_x100 / 100.0, 0, 'f', 2);
|
||||
}
|
||||
return QObject::tr("Ortho L%1").arg(sig.ortho_layer);
|
||||
}
|
||||
|
||||
bool RuntimeElementSignaturesEqual(const ShaderHunter::RuntimeElementSignature& lhs,
|
||||
const ShaderHunter::RuntimeElementSignature& rhs)
|
||||
{
|
||||
return lhs.valid == rhs.valid && lhs.perspective == rhs.perspective &&
|
||||
lhs.perspective_hfov_x100 == rhs.perspective_hfov_x100 &&
|
||||
lhs.perspective_vfov_x100 == rhs.perspective_vfov_x100 &&
|
||||
lhs.perspective_near_x1000 == rhs.perspective_near_x1000 &&
|
||||
lhs.perspective_far_x100 == rhs.perspective_far_x100 &&
|
||||
lhs.ortho_left_x100 == rhs.ortho_left_x100 &&
|
||||
lhs.ortho_right_x100 == rhs.ortho_right_x100 &&
|
||||
lhs.ortho_top_x100 == rhs.ortho_top_x100 &&
|
||||
lhs.ortho_bottom_x100 == rhs.ortho_bottom_x100 &&
|
||||
lhs.ortho_layer == rhs.ortho_layer &&
|
||||
lhs.viewport_x == rhs.viewport_x &&
|
||||
lhs.viewport_y == rhs.viewport_y &&
|
||||
lhs.viewport_width == rhs.viewport_width &&
|
||||
lhs.viewport_height == rhs.viewport_height &&
|
||||
lhs.scissor_left == rhs.scissor_left &&
|
||||
lhs.scissor_top == rhs.scissor_top &&
|
||||
lhs.scissor_right == rhs.scissor_right &&
|
||||
lhs.scissor_bottom == rhs.scissor_bottom &&
|
||||
lhs.alpha_test_hex == rhs.alpha_test_hex &&
|
||||
lhs.ztest == rhs.ztest && lhs.zupdate == rhs.zupdate && lhs.zfunc == rhs.zfunc &&
|
||||
lhs.blend_color_update == rhs.blend_color_update &&
|
||||
lhs.blend_alpha_update == rhs.blend_alpha_update;
|
||||
}
|
||||
|
||||
QString FormatCandidateLabel(const ImportShaderFindElementDialog::CandidateRow& row)
|
||||
{
|
||||
const auto& sig = row.candidate.signature;
|
||||
const auto& draw = row.candidate.representative_draw;
|
||||
return QObject::tr("%1 | VP %2,%3 %4x%5 | SC %6,%7 %8,%9 | Draw #%10 | Score %11 | %12\n"
|
||||
"Stage: VS %13 | PS %14 | GS %15 | Tex %16")
|
||||
.arg(FormatProjectionLabel(sig))
|
||||
.arg(sig.viewport_x)
|
||||
.arg(sig.viewport_y)
|
||||
.arg(sig.viewport_width)
|
||||
.arg(sig.viewport_height)
|
||||
.arg(sig.scissor_left)
|
||||
.arg(sig.scissor_top)
|
||||
.arg(sig.scissor_right)
|
||||
.arg(sig.scissor_bottom)
|
||||
.arg(draw.draw_index + 1)
|
||||
.arg(row.score)
|
||||
.arg(row.reason_summary)
|
||||
.arg(static_cast<qulonglong>(draw.vs_hash), 16, 16, QLatin1Char('0'))
|
||||
.arg(static_cast<qulonglong>(draw.ps_hash), 16, 16, QLatin1Char('0'))
|
||||
.arg(static_cast<qulonglong>(draw.gs_hash), 16, 16, QLatin1Char('0'))
|
||||
.arg(FormatTextureSummary(draw.textures));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ImportShaderFindElementDialog::ImportShaderFindElementDialog(
|
||||
const std::string& game_id, const ShaderHunter::ShaderOverride& source_shader, QWidget* parent)
|
||||
: QDialog(parent), m_game_id(game_id), m_source_shader(source_shader)
|
||||
{
|
||||
setWindowTitle(tr("Import Shader -> Find Element"));
|
||||
setMinimumWidth(620);
|
||||
|
||||
auto& manager = ElementsGroupManager::GetInstance();
|
||||
manager.SetPopupOpen(true);
|
||||
if (!m_game_id.empty())
|
||||
manager.LoadOverridesIfNeeded(m_game_id);
|
||||
|
||||
CreateWidgets();
|
||||
ConnectWidgets();
|
||||
|
||||
m_update_timer = new QTimer(this);
|
||||
connect(m_update_timer, &QTimer::timeout, this, &ImportShaderFindElementDialog::UpdateDisplay);
|
||||
m_update_timer->start(100);
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
ImportShaderFindElementDialog::~ImportShaderFindElementDialog() = default;
|
||||
|
||||
void ImportShaderFindElementDialog::CreateWidgets()
|
||||
{
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
|
||||
const QString shader_name = QString::fromStdString(m_source_shader.name);
|
||||
m_info_label = new QLabel(
|
||||
tr("Find a live runtime element seed candidate that corresponds to the saved shader '%1'.\n"
|
||||
"Candidates are scored using the shader anchor and copied texture filters.")
|
||||
.arg(shader_name));
|
||||
m_info_label->setWordWrap(true);
|
||||
layout->addWidget(m_info_label);
|
||||
|
||||
m_matching_only_check = new QCheckBox(tr("Show Matching Candidates Only"));
|
||||
m_matching_only_check->setChecked(true);
|
||||
layout->addWidget(m_matching_only_check);
|
||||
|
||||
m_candidate_list = new QListWidget;
|
||||
m_candidate_list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
layout->addWidget(m_candidate_list);
|
||||
|
||||
m_status_label = new QLabel(tr("Waiting for live seed candidates..."));
|
||||
m_status_label->setWordWrap(true);
|
||||
layout->addWidget(m_status_label);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel);
|
||||
m_open_hunting_button = buttons->addButton(tr("Open Elements Hunting"),
|
||||
QDialogButtonBox::ActionRole);
|
||||
m_use_candidate_button =
|
||||
buttons->addButton(tr("Use Selected Candidate"), QDialogButtonBox::AcceptRole);
|
||||
m_use_candidate_button->setEnabled(false);
|
||||
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(m_use_candidate_button, &QPushButton::clicked, this, &QDialog::accept);
|
||||
}
|
||||
|
||||
void ImportShaderFindElementDialog::ConnectWidgets()
|
||||
{
|
||||
connect(m_matching_only_check, &QCheckBox::toggled, this,
|
||||
&ImportShaderFindElementDialog::UpdateDisplay);
|
||||
connect(m_candidate_list, &QListWidget::itemSelectionChanged, this,
|
||||
&ImportShaderFindElementDialog::OnSelectionChanged);
|
||||
connect(m_open_hunting_button, &QPushButton::clicked, this, [this]() {
|
||||
m_open_elements_hunting = true;
|
||||
reject();
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<ElementsGroupManager::SeedCandidate> ImportShaderFindElementDialog::GetSelectedCandidate() const
|
||||
{
|
||||
const auto items = m_candidate_list->selectedItems();
|
||||
if (items.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const int row = items[0]->data(Qt::UserRole).toInt();
|
||||
if (row < 0 || row >= static_cast<int>(m_rows.size()))
|
||||
return std::nullopt;
|
||||
return m_rows[static_cast<size_t>(row)].candidate;
|
||||
}
|
||||
|
||||
bool ImportShaderFindElementDialog::ShouldOpenElementsHunting() const
|
||||
{
|
||||
return m_open_elements_hunting;
|
||||
}
|
||||
|
||||
void ImportShaderFindElementDialog::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
ElementsGroupManager::GetInstance().SetPopupOpen(false);
|
||||
QDialog::closeEvent(event);
|
||||
}
|
||||
|
||||
ImportShaderFindElementDialog::CandidateRow ImportShaderFindElementDialog::BuildCandidateRow(
|
||||
const ElementsGroupManager::SeedCandidate& candidate) const
|
||||
{
|
||||
CandidateRow row;
|
||||
row.candidate = candidate;
|
||||
|
||||
const auto& draw = candidate.representative_draw;
|
||||
const u64 draw_hash = draw.GetHash(m_source_shader.type);
|
||||
const u64 draw_family = draw.GetFamily(m_source_shader.type);
|
||||
|
||||
if (m_source_shader.hash_family_match && m_source_shader.family_signature != 0)
|
||||
{
|
||||
row.shader_match = (draw_family != 0 && draw_family == m_source_shader.family_signature);
|
||||
row.reason_summary = row.shader_match ? tr("shader family match") : tr("shader family mismatch");
|
||||
}
|
||||
else
|
||||
{
|
||||
row.shader_match = (draw_hash != 0 && draw_hash == m_source_shader.hash);
|
||||
row.reason_summary = row.shader_match ? tr("shader exact match") : tr("shader exact mismatch");
|
||||
}
|
||||
|
||||
row.texture_match = true;
|
||||
if (!m_source_shader.texture_hashes.empty())
|
||||
{
|
||||
bool any_texture_match = false;
|
||||
for (u64 texture_hash : draw.textures)
|
||||
{
|
||||
if (texture_hash == 0)
|
||||
continue;
|
||||
if (std::find(m_source_shader.texture_hashes.begin(), m_source_shader.texture_hashes.end(),
|
||||
texture_hash) != m_source_shader.texture_hashes.end())
|
||||
{
|
||||
any_texture_match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
row.texture_match =
|
||||
m_source_shader.texture_hashes_excluded ? !any_texture_match : any_texture_match;
|
||||
row.reason_summary += row.texture_match ?
|
||||
(m_source_shader.texture_hashes_excluded ?
|
||||
tr(", excluded textures clear") :
|
||||
tr(", texture match")) :
|
||||
(m_source_shader.texture_hashes_excluded ?
|
||||
tr(", excluded texture present") :
|
||||
tr(", texture mismatch"));
|
||||
}
|
||||
|
||||
row.full_match = row.shader_match && row.texture_match;
|
||||
row.score = 0;
|
||||
if (row.shader_match)
|
||||
row.score += 100;
|
||||
if (row.texture_match)
|
||||
row.score += m_source_shader.texture_hashes.empty() ? 0 : 25;
|
||||
row.score += std::min(candidate.occurrence_count, 9);
|
||||
return row;
|
||||
}
|
||||
|
||||
void ImportShaderFindElementDialog::UpdateDisplay()
|
||||
{
|
||||
std::optional<ShaderHunter::RuntimeElementSignature> selected_signature;
|
||||
if (const auto selected = GetSelectedCandidate(); selected.has_value())
|
||||
selected_signature = selected->signature;
|
||||
|
||||
std::vector<CandidateRow> rows;
|
||||
rows.reserve(ElementsGroupManager::GetInstance().GetSeedCandidates().size());
|
||||
|
||||
for (const auto& candidate : ElementsGroupManager::GetInstance().GetSeedCandidates())
|
||||
{
|
||||
CandidateRow row = BuildCandidateRow(candidate);
|
||||
if (!m_matching_only_check->isChecked() || row.full_match)
|
||||
rows.push_back(std::move(row));
|
||||
}
|
||||
|
||||
std::sort(rows.begin(), rows.end(), [](const CandidateRow& lhs, const CandidateRow& rhs) {
|
||||
if (lhs.full_match != rhs.full_match)
|
||||
return lhs.full_match > rhs.full_match;
|
||||
if (lhs.score != rhs.score)
|
||||
return lhs.score > rhs.score;
|
||||
return lhs.candidate.representative_draw.draw_index < rhs.candidate.representative_draw.draw_index;
|
||||
});
|
||||
|
||||
m_rows = rows;
|
||||
|
||||
{
|
||||
const QSignalBlocker blocker(m_candidate_list);
|
||||
m_candidate_list->clear();
|
||||
int row_to_select = -1;
|
||||
for (size_t i = 0; i < m_rows.size(); ++i)
|
||||
{
|
||||
auto* item = new QListWidgetItem(FormatCandidateLabel(m_rows[i]));
|
||||
item->setData(Qt::UserRole, static_cast<int>(i));
|
||||
if (!m_rows[i].full_match)
|
||||
item->setForeground(QBrush(QColor(150, 150, 150)));
|
||||
m_candidate_list->addItem(item);
|
||||
if (selected_signature.has_value() &&
|
||||
RuntimeElementSignaturesEqual(m_rows[i].candidate.signature, *selected_signature))
|
||||
{
|
||||
row_to_select = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
if (row_to_select >= 0)
|
||||
m_candidate_list->setCurrentRow(row_to_select);
|
||||
else if (!m_rows.empty())
|
||||
m_candidate_list->setCurrentRow(0);
|
||||
}
|
||||
|
||||
if (m_rows.empty())
|
||||
{
|
||||
m_status_label->setText(
|
||||
tr("No live seed candidates currently match this shader import.\n"
|
||||
"Keep the game rendering this scene, disable 'Show Matching Candidates Only', or open "
|
||||
"Elements Hunting for manual inspection."));
|
||||
}
|
||||
else
|
||||
{
|
||||
const int matching_count = static_cast<int>(std::count_if(
|
||||
m_rows.begin(), m_rows.end(), [](const CandidateRow& row) { return row.full_match; }));
|
||||
m_status_label->setText(
|
||||
tr("Candidates: %1 total, %2 matching.\nSelect one candidate to use as the runtime "
|
||||
"element seed.")
|
||||
.arg(m_rows.size())
|
||||
.arg(matching_count));
|
||||
}
|
||||
|
||||
OnSelectionChanged();
|
||||
}
|
||||
|
||||
void ImportShaderFindElementDialog::OnSelectionChanged()
|
||||
{
|
||||
m_use_candidate_button->setEnabled(!m_candidate_list->selectedItems().empty());
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "VideoCommon/ElementsGroupManager.h"
|
||||
#include "VideoCommon/ShaderHunter.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QPushButton;
|
||||
class QTimer;
|
||||
|
||||
class ImportShaderFindElementDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct CandidateRow
|
||||
{
|
||||
ElementsGroupManager::SeedCandidate candidate;
|
||||
bool shader_match = false;
|
||||
bool texture_match = false;
|
||||
bool full_match = false;
|
||||
int score = 0;
|
||||
QString reason_summary;
|
||||
};
|
||||
|
||||
explicit ImportShaderFindElementDialog(const std::string& game_id,
|
||||
const ShaderHunter::ShaderOverride& source_shader,
|
||||
QWidget* parent = nullptr);
|
||||
~ImportShaderFindElementDialog() override;
|
||||
|
||||
std::optional<ElementsGroupManager::SeedCandidate> GetSelectedCandidate() const;
|
||||
bool ShouldOpenElementsHunting() const;
|
||||
|
||||
private:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void CreateWidgets();
|
||||
void ConnectWidgets();
|
||||
void UpdateDisplay();
|
||||
void OnSelectionChanged();
|
||||
CandidateRow BuildCandidateRow(const ElementsGroupManager::SeedCandidate& candidate) const;
|
||||
|
||||
std::string m_game_id;
|
||||
ShaderHunter::ShaderOverride m_source_shader;
|
||||
QLabel* m_info_label = nullptr;
|
||||
QCheckBox* m_matching_only_check = nullptr;
|
||||
QListWidget* m_candidate_list = nullptr;
|
||||
QLabel* m_status_label = nullptr;
|
||||
QPushButton* m_open_hunting_button = nullptr;
|
||||
QPushButton* m_use_candidate_button = nullptr;
|
||||
QTimer* m_update_timer = nullptr;
|
||||
bool m_open_elements_hunting = false;
|
||||
std::vector<CandidateRow> m_rows;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/Mapping/HotkeyVR.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
#include "Core/HotkeyManager.h"
|
||||
|
||||
HotkeyVR::HotkeyVR(MappingWindow* window) : MappingWidget(window)
|
||||
{
|
||||
CreateMainLayout();
|
||||
}
|
||||
|
||||
void HotkeyVR::CreateMainLayout()
|
||||
{
|
||||
m_main_layout = new QHBoxLayout();
|
||||
|
||||
m_main_layout->addWidget(CreateGroupBox(tr("VR"), HotkeyManagerEmu::GetHotkeyGroup(HKGP_VR)));
|
||||
m_main_layout->addWidget(
|
||||
CreateGroupBox(tr("Shader"), HotkeyManagerEmu::GetHotkeyGroup(HKGP_VR_SHADER)));
|
||||
|
||||
setLayout(m_main_layout);
|
||||
}
|
||||
|
||||
InputConfig* HotkeyVR::GetConfig()
|
||||
{
|
||||
return HotkeyManagerEmu::GetConfig();
|
||||
}
|
||||
|
||||
void HotkeyVR::LoadSettings()
|
||||
{
|
||||
HotkeyManagerEmu::LoadConfig();
|
||||
}
|
||||
|
||||
void HotkeyVR::SaveSettings()
|
||||
{
|
||||
HotkeyManagerEmu::GetConfig()->SaveConfig();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "DolphinQt/Config/Mapping/MappingWidget.h"
|
||||
|
||||
class QHBoxLayout;
|
||||
|
||||
class HotkeyVR final : public MappingWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HotkeyVR(MappingWindow* window);
|
||||
|
||||
InputConfig* GetConfig() override;
|
||||
|
||||
private:
|
||||
void LoadSettings() override;
|
||||
void SaveSettings() override;
|
||||
void CreateMainLayout();
|
||||
|
||||
QHBoxLayout* m_main_layout;
|
||||
};
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
|
||||
#include "Common/MathUtil.h"
|
||||
|
||||
#include "Core/HW/Wiimote.h"
|
||||
#include "Core/HW/WiimoteEmu/Camera.h"
|
||||
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
|
||||
|
||||
#include "InputCommon/ControllerEmu/Control/Control.h"
|
||||
#include "InputCommon/ControllerEmu/ControlGroup/Cursor.h"
|
||||
@@ -61,6 +63,26 @@ QPen GetInputDotPen(QPen pen)
|
||||
return GetCosmeticPen(pen);
|
||||
}
|
||||
|
||||
std::optional<Wiimote::OpenXRWiiRemoteState>
|
||||
GetOpenXRWiiRemoteStateForGroup(int wiimote_port, const std::string& group_name,
|
||||
bool prefer_left_hand = false)
|
||||
{
|
||||
if (prefer_left_hand)
|
||||
return Wiimote::GetOpenXRHandState(true);
|
||||
|
||||
if (wiimote_port < WIIMOTE_CHAN_0 || wiimote_port >= MAX_WIIMOTES)
|
||||
return std::nullopt;
|
||||
|
||||
if (group_name != WiimoteEmu::Wiimote::IR_GROUP &&
|
||||
group_name != WiimoteEmu::Wiimote::ACCELEROMETER_GROUP &&
|
||||
group_name != WiimoteEmu::Wiimote::GYROSCOPE_GROUP)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Wiimote::GetOpenXRWiiRemoteState(static_cast<unsigned int>(wiimote_port));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QPen MappingIndicator::GetBBoxPen() const
|
||||
@@ -327,6 +349,18 @@ QColor CursorIndicator::GetGateBrushColor() const
|
||||
|
||||
void CursorIndicator::Draw()
|
||||
{
|
||||
if (const auto openxr_state =
|
||||
GetOpenXRWiiRemoteStateForGroup(m_wiimote_port, m_cursor_group.name))
|
||||
{
|
||||
const auto adj_coord =
|
||||
openxr_state->ir_visible
|
||||
? std::make_optional(ControllerEmu::ReshapableInput::ReshapeData{openxr_state->ir_x,
|
||||
openxr_state->ir_y})
|
||||
: std::nullopt;
|
||||
DrawReshapableInput(m_cursor_group, adj_coord);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto adj_coord = m_cursor_group.GetState(true);
|
||||
|
||||
DrawReshapableInput(m_cursor_group,
|
||||
@@ -707,7 +741,13 @@ void ShakeMappingIndicator::Draw()
|
||||
|
||||
void AccelerometerMappingIndicator::Draw()
|
||||
{
|
||||
const auto accel_state = m_accel_group.GetState();
|
||||
auto accel_state = m_accel_group.GetState();
|
||||
if (const auto openxr_state =
|
||||
GetOpenXRWiiRemoteStateForGroup(m_wiimote_port, m_accel_group.name, m_prefer_left_hand))
|
||||
{
|
||||
accel_state = Common::Vec3{openxr_state->acceleration[0], openxr_state->acceleration[1],
|
||||
openxr_state->acceleration[2]};
|
||||
}
|
||||
const auto state = accel_state.value_or(Common::Vec3{});
|
||||
|
||||
QPainter p(this);
|
||||
@@ -783,7 +823,12 @@ void AccelerometerMappingIndicator::Draw()
|
||||
|
||||
void GyroMappingIndicator::Update(float elapsed_seconds)
|
||||
{
|
||||
const auto gyro_state = m_gyro_group.GetState();
|
||||
auto gyro_state = m_gyro_group.GetState();
|
||||
if (const auto openxr_state = GetOpenXRWiiRemoteStateForGroup(m_wiimote_port, m_gyro_group.name))
|
||||
{
|
||||
gyro_state = Common::Vec3{openxr_state->angular_velocity[0], openxr_state->angular_velocity[1],
|
||||
openxr_state->angular_velocity[2]};
|
||||
}
|
||||
const auto angular_velocity = gyro_state.value_or(Common::Vec3{});
|
||||
m_state *= WiimoteEmu::GetRotationFromGyroscope(angular_velocity * Common::Vec3(-1, +1, -1) *
|
||||
elapsed_seconds);
|
||||
@@ -805,8 +850,16 @@ void GyroMappingIndicator::Update(float elapsed_seconds)
|
||||
|
||||
void GyroMappingIndicator::Draw()
|
||||
{
|
||||
const auto gyro_state = m_gyro_group.GetState();
|
||||
const auto raw_gyro_state = m_gyro_group.GetRawState();
|
||||
auto gyro_state = m_gyro_group.GetState();
|
||||
auto raw_gyro_state = m_gyro_group.GetRawState();
|
||||
if (const auto openxr_state = GetOpenXRWiiRemoteStateForGroup(m_wiimote_port, m_gyro_group.name))
|
||||
{
|
||||
const Common::Vec3 openxr_angular_velocity{
|
||||
openxr_state->angular_velocity[0], openxr_state->angular_velocity[1],
|
||||
openxr_state->angular_velocity[2]};
|
||||
gyro_state = openxr_angular_velocity;
|
||||
raw_gyro_state = openxr_angular_velocity;
|
||||
}
|
||||
const auto angular_velocity = gyro_state.value_or(Common::Vec3{});
|
||||
const auto jitter = raw_gyro_state - m_previous_velocity;
|
||||
m_previous_velocity = raw_gyro_state;
|
||||
|
||||
@@ -134,7 +134,10 @@ private:
|
||||
class CursorIndicator : public ReshapableInputIndicator
|
||||
{
|
||||
public:
|
||||
explicit CursorIndicator(ControllerEmu::Cursor& cursor) : m_cursor_group(cursor) {}
|
||||
explicit CursorIndicator(ControllerEmu::Cursor& cursor, int wiimote_port = -1)
|
||||
: m_cursor_group(cursor), m_wiimote_port(wiimote_port)
|
||||
{
|
||||
}
|
||||
|
||||
QColor GetGateBrushColor() const final;
|
||||
|
||||
@@ -142,6 +145,7 @@ private:
|
||||
void Draw() override;
|
||||
|
||||
ControllerEmu::Cursor& m_cursor_group;
|
||||
int m_wiimote_port = -1;
|
||||
};
|
||||
|
||||
class MixedTriggersIndicator : public MappingIndicator
|
||||
@@ -197,8 +201,11 @@ private:
|
||||
class AccelerometerMappingIndicator : public SquareIndicator
|
||||
{
|
||||
public:
|
||||
explicit AccelerometerMappingIndicator(ControllerEmu::IMUAccelerometer& accel)
|
||||
: m_accel_group(accel)
|
||||
explicit AccelerometerMappingIndicator(ControllerEmu::IMUAccelerometer& accel,
|
||||
int wiimote_port = -1,
|
||||
bool prefer_left_hand = false)
|
||||
: m_accel_group(accel), m_wiimote_port(wiimote_port),
|
||||
m_prefer_left_hand(prefer_left_hand)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -206,18 +213,24 @@ private:
|
||||
void Draw() override;
|
||||
|
||||
ControllerEmu::IMUAccelerometer& m_accel_group;
|
||||
int m_wiimote_port = -1;
|
||||
bool m_prefer_left_hand = false;
|
||||
};
|
||||
|
||||
class GyroMappingIndicator : public SquareIndicator
|
||||
{
|
||||
public:
|
||||
explicit GyroMappingIndicator(ControllerEmu::IMUGyroscope& gyro) : m_gyro_group(gyro) {}
|
||||
explicit GyroMappingIndicator(ControllerEmu::IMUGyroscope& gyro, int wiimote_port = -1)
|
||||
: m_gyro_group(gyro), m_wiimote_port(wiimote_port)
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
void Draw() override;
|
||||
void Update(float elapsed_seconds) override;
|
||||
|
||||
ControllerEmu::IMUGyroscope& m_gyro_group;
|
||||
int m_wiimote_port = -1;
|
||||
Common::Quaternion m_state = Common::Quaternion::Identity();
|
||||
Common::Vec3 m_previous_velocity = {};
|
||||
float m_stable_time = 0;
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
#include "DolphinQt/Config/Mapping/MappingNumeric.h"
|
||||
#include "DolphinQt/Config/Mapping/MappingWindow.h"
|
||||
|
||||
#include "Core/HW/Wiimote.h"
|
||||
#include "Core/HW/WiimoteEmu/Extension/Nunchuk.h"
|
||||
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
|
||||
#include "Core/Config/WiimoteSettings.h"
|
||||
|
||||
#include "InputCommon/ControllerEmu/Control/Control.h"
|
||||
#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
|
||||
#include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h"
|
||||
@@ -58,6 +63,19 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
||||
|
||||
MappingIndicator* indicator = nullptr;
|
||||
|
||||
const bool is_openxr_wiimote_source =
|
||||
GetPort() >= WIIMOTE_CHAN_0 && GetPort() < MAX_WIIMOTES &&
|
||||
Config::Get(Config::GetInfoForWiimoteSource(GetPort())) == WiimoteSource::OpenXR;
|
||||
const bool is_openxr_wiimote_point_group =
|
||||
is_openxr_wiimote_source &&
|
||||
group == Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Point);
|
||||
const bool is_openxr_wiimote_imu_accel_group =
|
||||
is_openxr_wiimote_source &&
|
||||
group == Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer);
|
||||
const bool is_openxr_wiimote_imu_gyro_group =
|
||||
is_openxr_wiimote_source &&
|
||||
group == Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope);
|
||||
|
||||
switch (group->type)
|
||||
{
|
||||
case ControllerEmu::GroupType::Shake:
|
||||
@@ -73,7 +91,7 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
||||
break;
|
||||
|
||||
case ControllerEmu::GroupType::Cursor:
|
||||
indicator = new CursorIndicator(*static_cast<ControllerEmu::Cursor*>(group));
|
||||
indicator = new CursorIndicator(*static_cast<ControllerEmu::Cursor*>(group), GetPort());
|
||||
break;
|
||||
|
||||
case ControllerEmu::GroupType::Force:
|
||||
@@ -81,13 +99,49 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
||||
break;
|
||||
|
||||
case ControllerEmu::GroupType::IMUAccelerometer:
|
||||
indicator =
|
||||
new AccelerometerMappingIndicator(*static_cast<ControllerEmu::IMUAccelerometer*>(group));
|
||||
{
|
||||
int indicator_port = -1;
|
||||
bool prefer_left_hand = false;
|
||||
if (GetPort() >= WIIMOTE_CHAN_0 && GetPort() < MAX_WIIMOTES)
|
||||
{
|
||||
auto* const wiimote_group = Wiimote::GetWiimoteGroup(
|
||||
GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer);
|
||||
if (group == wiimote_group)
|
||||
{
|
||||
indicator_port = GetPort();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto* const nunchuk_group =
|
||||
Wiimote::GetNunchukGroup(GetPort(), WiimoteEmu::NunchukGroup::IMUAccelerometer);
|
||||
if (group == nunchuk_group)
|
||||
{
|
||||
indicator_port = GetPort();
|
||||
prefer_left_hand = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indicator = new AccelerometerMappingIndicator(
|
||||
*static_cast<ControllerEmu::IMUAccelerometer*>(group), indicator_port, prefer_left_hand);
|
||||
break;
|
||||
}
|
||||
|
||||
case ControllerEmu::GroupType::IMUGyroscope:
|
||||
indicator = new GyroMappingIndicator(*static_cast<ControllerEmu::IMUGyroscope*>(group));
|
||||
{
|
||||
int indicator_port = -1;
|
||||
if (GetPort() >= WIIMOTE_CHAN_0 && GetPort() < MAX_WIIMOTES)
|
||||
{
|
||||
auto* const wiimote_group =
|
||||
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope);
|
||||
if (group == wiimote_group)
|
||||
indicator_port = GetPort();
|
||||
}
|
||||
|
||||
indicator = new GyroMappingIndicator(*static_cast<ControllerEmu::IMUGyroscope*>(group),
|
||||
indicator_port);
|
||||
break;
|
||||
}
|
||||
|
||||
case ControllerEmu::GroupType::Stick:
|
||||
indicator = new AnalogStickIndicator(*static_cast<ControllerEmu::ReshapableInput*>(group));
|
||||
@@ -144,10 +198,11 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
||||
// "Calibrate" button.
|
||||
if (indicator != nullptr)
|
||||
{
|
||||
const bool need_calibration = group->type == ControllerEmu::GroupType::Cursor ||
|
||||
const bool need_calibration = !is_openxr_wiimote_point_group &&
|
||||
(group->type == ControllerEmu::GroupType::Cursor ||
|
||||
group->type == ControllerEmu::GroupType::Stick ||
|
||||
group->type == ControllerEmu::GroupType::Tilt ||
|
||||
group->type == ControllerEmu::GroupType::Force;
|
||||
group->type == ControllerEmu::GroupType::Force);
|
||||
|
||||
if (need_calibration)
|
||||
{
|
||||
@@ -159,8 +214,16 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& control : group->controls)
|
||||
for (size_t i = 0; i < group->controls.size(); ++i)
|
||||
{
|
||||
if (is_openxr_wiimote_point_group && i < 4)
|
||||
continue;
|
||||
if (is_openxr_wiimote_imu_accel_group || is_openxr_wiimote_imu_gyro_group)
|
||||
continue;
|
||||
|
||||
auto& control = group->controls[i];
|
||||
CreateControl(control.get(), form_layout, !indicator);
|
||||
}
|
||||
|
||||
AddSettingWidgets(form_layout, group, ControllerEmu::SettingVisibility::Normal);
|
||||
|
||||
@@ -176,7 +239,7 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
||||
[this, group] { ShowAdvancedControlGroupDialog(group); });
|
||||
}
|
||||
|
||||
if (group->type == ControllerEmu::GroupType::Cursor)
|
||||
if (group->type == ControllerEmu::GroupType::Cursor && !is_openxr_wiimote_point_group)
|
||||
{
|
||||
QPushButton* mouse_button = new QPushButton(tr("Use Mouse Controlled Pointing"));
|
||||
form_layout->insertRow(2, mouse_button);
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
|
||||
#include "Core/HW/SI/SI.h"
|
||||
#include "Core/HW/SI/SI_DeviceAMBaseboard.h"
|
||||
#include "Core/Config/WiimoteSettings.h"
|
||||
#include "Core/HW/Wiimote.h"
|
||||
|
||||
#include "DolphinQt/Config/Mapping/FreeLookGeneral.h"
|
||||
#include "DolphinQt/Config/Mapping/FreeLookRotation.h"
|
||||
@@ -45,8 +47,10 @@
|
||||
#include "DolphinQt/Config/Mapping/HotkeyStatesOther.h"
|
||||
#include "DolphinQt/Config/Mapping/HotkeyTAS.h"
|
||||
#include "DolphinQt/Config/Mapping/HotkeyUSBEmu.h"
|
||||
#include "DolphinQt/Config/Mapping/HotkeyVR.h"
|
||||
#include "DolphinQt/Config/Mapping/HotkeyWii.h"
|
||||
#include "DolphinQt/Config/Mapping/MappingCommon.h"
|
||||
#include "DolphinQt/Config/Mapping/OpenXRWiimoteConfigSessionController.h"
|
||||
#include "DolphinQt/Config/Mapping/WiimoteEmuExtension.h"
|
||||
#include "DolphinQt/Config/Mapping/WiimoteEmuExtensionMotionInput.h"
|
||||
#include "DolphinQt/Config/Mapping/WiimoteEmuExtensionMotionSimulation.h"
|
||||
@@ -91,6 +95,9 @@ MappingWindow::MappingWindow(QWidget* parent, Type type, int port_num)
|
||||
const auto lock = GetController()->GetStateLock();
|
||||
emit ConfigChanged();
|
||||
|
||||
if (m_mapping_type == Type::MAPPING_WIIMOTE_EMU && m_is_openxr_wiimote)
|
||||
m_openxr_config_session_controller = new OpenXRWiimoteConfigSessionController(this, m_port);
|
||||
|
||||
auto* filter = new WindowActivationEventFilter(this);
|
||||
installEventFilter(filter);
|
||||
|
||||
@@ -442,6 +449,7 @@ void MappingWindow::UpdateDeviceList()
|
||||
|
||||
void MappingWindow::SetMappingType(MappingWindow::Type type)
|
||||
{
|
||||
m_mapping_type = type;
|
||||
MappingWidget* widget;
|
||||
|
||||
switch (type)
|
||||
@@ -472,17 +480,20 @@ void MappingWindow::SetMappingType(MappingWindow::Type type)
|
||||
break;
|
||||
case Type::MAPPING_WIIMOTE_EMU:
|
||||
{
|
||||
m_is_openxr_wiimote =
|
||||
Config::Get(Config::GetInfoForWiimoteSource(GetPort())) == WiimoteSource::OpenXR;
|
||||
auto* extension = new WiimoteEmuExtension(this);
|
||||
auto* extension_motion_input = new WiimoteEmuExtensionMotionInput(this);
|
||||
auto* extension_motion_simulation = new WiimoteEmuExtensionMotionSimulation(this);
|
||||
widget = new WiimoteEmuGeneral(this, extension);
|
||||
setWindowTitle(tr("Wii Remote %1").arg(GetPort() + 1));
|
||||
AddWidget(tr("General and Options"), widget);
|
||||
AddWidget(tr("Motion Simulation"), new WiimoteEmuMotionControl(this));
|
||||
if (!m_is_openxr_wiimote)
|
||||
AddWidget(tr("Motion Simulation"), new WiimoteEmuMotionControl(this));
|
||||
AddWidget(tr("Motion Input"), new WiimoteEmuMotionControlIMU(this));
|
||||
AddWidget(tr("Extension"), extension);
|
||||
m_extension_motion_simulation_tab =
|
||||
AddWidget(EXTENSION_MOTION_SIMULATION_TAB_NAME, extension_motion_simulation);
|
||||
m_extension_tab = AddWidget(tr("Extension"), extension);
|
||||
m_extension_motion_simulation_tab = AddWidget(EXTENSION_MOTION_SIMULATION_TAB_NAME,
|
||||
extension_motion_simulation);
|
||||
m_extension_motion_input_tab =
|
||||
AddWidget(EXTENSION_MOTION_INPUT_TAB_NAME, extension_motion_input);
|
||||
// Hide tabs by default. "Nunchuk" selection triggers an event to show them.
|
||||
@@ -502,6 +513,7 @@ void MappingWindow::SetMappingType(MappingWindow::Type type)
|
||||
AddWidget(tr("Wii and Wii Remote"), new HotkeyWii(this));
|
||||
AddWidget(tr("Controller Profile"), new HotkeyControllerProfile(this));
|
||||
AddWidget(tr("Graphics"), new HotkeyGraphics(this));
|
||||
AddWidget(tr("VR"), new HotkeyVR(this));
|
||||
AddWidget(tr("USB Emulation"), new HotkeyUSBEmu(this));
|
||||
// i18n: Stereoscopic 3D
|
||||
AddWidget(tr("3D"), new Hotkey3D(this));
|
||||
@@ -615,19 +627,28 @@ void MappingWindow::OnClearFieldsPressed()
|
||||
|
||||
void MappingWindow::ShowExtensionMotionTabs(bool show)
|
||||
{
|
||||
const int motion_sim_tab_index =
|
||||
m_extension_motion_simulation_tab ? m_tab_widget->indexOf(m_extension_motion_simulation_tab) :
|
||||
-1;
|
||||
const int motion_input_tab_index = m_tab_widget->indexOf(m_extension_motion_input_tab);
|
||||
if (show)
|
||||
{
|
||||
m_tab_widget->addTab(m_extension_motion_simulation_tab, EXTENSION_MOTION_SIMULATION_TAB_NAME);
|
||||
m_tab_widget->addTab(m_extension_motion_input_tab, EXTENSION_MOTION_INPUT_TAB_NAME);
|
||||
if (!m_is_openxr_wiimote && m_extension_motion_simulation_tab && motion_sim_tab_index == -1)
|
||||
m_tab_widget->addTab(m_extension_motion_simulation_tab, EXTENSION_MOTION_SIMULATION_TAB_NAME);
|
||||
if (motion_input_tab_index == -1)
|
||||
m_tab_widget->addTab(m_extension_motion_input_tab, EXTENSION_MOTION_INPUT_TAB_NAME);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_tab_widget->removeTab(5);
|
||||
m_tab_widget->removeTab(4);
|
||||
if (motion_sim_tab_index != -1)
|
||||
m_tab_widget->removeTab(motion_sim_tab_index);
|
||||
if (motion_input_tab_index != -1)
|
||||
m_tab_widget->removeTab(motion_input_tab_index);
|
||||
}
|
||||
}
|
||||
|
||||
void MappingWindow::ActivateExtensionTab()
|
||||
{
|
||||
m_tab_widget->setCurrentIndex(3);
|
||||
if (m_extension_tab)
|
||||
m_tab_widget->setCurrentIndex(m_tab_widget->indexOf(m_extension_tab));
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
namespace ControllerEmu
|
||||
@@ -13,6 +14,7 @@ class EmulatedController;
|
||||
|
||||
class InputConfig;
|
||||
class MappingButton;
|
||||
class OpenXRWiimoteConfigSessionController;
|
||||
|
||||
class QComboBox;
|
||||
class QDialogButtonBox;
|
||||
@@ -129,12 +131,15 @@ private:
|
||||
QPushButton* m_reset_clear;
|
||||
|
||||
QTabWidget* m_tab_widget;
|
||||
QWidget* m_extension_tab = nullptr;
|
||||
QWidget* m_extension_motion_input_tab;
|
||||
QWidget* m_extension_motion_simulation_tab;
|
||||
QWidget* m_extension_motion_simulation_tab = nullptr;
|
||||
const QString EXTENSION_MOTION_INPUT_TAB_NAME = tr("Extension Motion Input");
|
||||
const QString EXTENSION_MOTION_SIMULATION_TAB_NAME = tr("Extension Motion Simulation");
|
||||
|
||||
Type m_mapping_type;
|
||||
const int m_port;
|
||||
bool m_is_openxr_wiimote = false;
|
||||
InputConfig* m_config;
|
||||
QPointer<OpenXRWiimoteConfigSessionController> m_openxr_config_session_controller;
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/Mapping/OpenXRWiimoteConfigSessionController.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QPen>
|
||||
#include <QTimer>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Core/Config/GraphicsSettings.h"
|
||||
#include "Core/Config/WiimoteSettings.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/HW/Wiimote.h"
|
||||
#include "Core/System.h"
|
||||
#include "DolphinQt/Config/Mapping/MappingWindow.h"
|
||||
#include "DolphinQt/Settings.h"
|
||||
#if defined(ENABLE_VR) && defined(_WIN32)
|
||||
#include "Common/VR/OpenXRInputState.h"
|
||||
#include "VideoCommon/VR/OpenXRManager.h"
|
||||
#include "VideoCommon/VR/OpenXRUtilitySession.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int OVERLAY_WIDTH = 1024;
|
||||
constexpr int OVERLAY_HEIGHT = 704;
|
||||
|
||||
#if defined(ENABLE_VR) && defined(_WIN32)
|
||||
QString BoolToDigit(bool value)
|
||||
{
|
||||
return value ? QStringLiteral("1") : QStringLiteral("0");
|
||||
}
|
||||
|
||||
QString BuildDigitalStateLine1(const Common::VR::OpenXRControllerState& controller)
|
||||
{
|
||||
return QObject::tr("Primary: %1 Secondary: %2 Menu: %3")
|
||||
.arg(BoolToDigit(controller.primary_button), BoolToDigit(controller.secondary_button),
|
||||
BoolToDigit(controller.menu_button));
|
||||
}
|
||||
|
||||
QString BuildDigitalStateLine2(const Common::VR::OpenXRControllerState& controller)
|
||||
{
|
||||
return QObject::tr("Trigger Btn: %1 Grip Btn: %2 Stick Btn: %3")
|
||||
.arg(BoolToDigit(controller.trigger_button), BoolToDigit(controller.squeeze_button),
|
||||
BoolToDigit(controller.thumbstick_button));
|
||||
}
|
||||
|
||||
QString BuildAnalogStateLine(const Common::VR::OpenXRControllerState& controller)
|
||||
{
|
||||
return QObject::tr("Trigger: %1 Grip: %2 Stick: (%3, %4)")
|
||||
.arg(QString::number(controller.trigger_value, 'f', 2),
|
||||
QString::number(controller.squeeze_value, 'f', 2),
|
||||
QString::number(controller.thumbstick_x, 'f', 2),
|
||||
QString::number(controller.thumbstick_y, 'f', 2));
|
||||
}
|
||||
|
||||
std::vector<u8> RenderOverlayImage(int port, const Common::VR::OpenXRInputSnapshot& snapshot)
|
||||
{
|
||||
QImage image(OVERLAY_WIDTH, OVERLAY_HEIGHT, QImage::Format_RGBA8888);
|
||||
image.fill(qRgba(0, 0, 0, 0));
|
||||
|
||||
QPainter painter(&image);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing, true);
|
||||
|
||||
const QRect background_rect(24, 24, OVERLAY_WIDTH - 48, OVERLAY_HEIGHT - 48);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(18, 22, 28, 220));
|
||||
painter.drawRoundedRect(background_rect, 24, 24);
|
||||
|
||||
QFont title_font = painter.font();
|
||||
title_font.setPointSize(28);
|
||||
title_font.setBold(true);
|
||||
painter.setFont(title_font);
|
||||
painter.setPen(QColor(245, 247, 250));
|
||||
painter.drawText(QRect(56, 54, OVERLAY_WIDTH - 112, 50), Qt::AlignLeft | Qt::AlignVCenter,
|
||||
QObject::tr("OpenXR Wii Remote Binding"));
|
||||
|
||||
QFont subtitle_font = painter.font();
|
||||
subtitle_font.setPointSize(18);
|
||||
subtitle_font.setBold(false);
|
||||
painter.setFont(subtitle_font);
|
||||
painter.setPen(QColor(180, 188, 198));
|
||||
painter.drawText(QRect(56, 114, OVERLAY_WIDTH - 112, 36), Qt::AlignLeft | Qt::AlignVCenter,
|
||||
QObject::tr("Wii Remote %1").arg(port + 1));
|
||||
|
||||
painter.drawText(QRect(56, 164, OVERLAY_WIDTH - 112, 36), Qt::AlignLeft | Qt::AlignVCenter,
|
||||
QObject::tr("Use the desktop window to bind controls"));
|
||||
|
||||
const std::array<QString, 2> labels = {QObject::tr("Left Controller"),
|
||||
QObject::tr("Right Controller")};
|
||||
|
||||
QFont status_font = painter.font();
|
||||
status_font.setPointSize(17);
|
||||
painter.setFont(status_font);
|
||||
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
const auto& controller = snapshot.controllers[i];
|
||||
const int top = 236 + i * 168;
|
||||
const QRect row_rect(56, top, OVERLAY_WIDTH - 112, 144);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(32, 38, 46, 210));
|
||||
painter.drawRoundedRect(row_rect, 16, 16);
|
||||
|
||||
painter.setBrush(controller.connected ? QColor(44, 182, 125) : QColor(196, 84, 84));
|
||||
painter.drawEllipse(QPoint(92, top + 28), 10, 10);
|
||||
|
||||
painter.setPen(QColor(238, 240, 243));
|
||||
painter.drawText(QRect(120, top + 2, 280, 52), Qt::AlignLeft | Qt::AlignVCenter, labels[i]);
|
||||
|
||||
painter.setPen(controller.connected ? QColor(129, 228, 180) : QColor(246, 151, 151));
|
||||
painter.drawText(QRect(OVERLAY_WIDTH - 300, top + 2, 220, 52),
|
||||
Qt::AlignRight | Qt::AlignVCenter,
|
||||
controller.connected ? QObject::tr("Connected") : QObject::tr("Waiting"));
|
||||
|
||||
QFont details_font = painter.font();
|
||||
details_font.setPointSize(13);
|
||||
painter.setFont(details_font);
|
||||
painter.setPen(QColor(208, 214, 221));
|
||||
painter.drawText(QRect(88, top + 52, OVERLAY_WIDTH - 176, 24), Qt::AlignLeft | Qt::AlignVCenter,
|
||||
BuildDigitalStateLine1(controller));
|
||||
painter.drawText(QRect(88, top + 82, OVERLAY_WIDTH - 176, 24), Qt::AlignLeft | Qt::AlignVCenter,
|
||||
BuildDigitalStateLine2(controller));
|
||||
painter.setPen(QColor(162, 171, 182));
|
||||
painter.drawText(QRect(88, top + 112, OVERLAY_WIDTH - 176, 24), Qt::AlignLeft | Qt::AlignVCenter,
|
||||
BuildAnalogStateLine(controller));
|
||||
|
||||
painter.setFont(status_font);
|
||||
}
|
||||
|
||||
painter.setPen(QColor(140, 148, 160));
|
||||
painter.drawText(QRect(56, OVERLAY_HEIGHT - 60, OVERLAY_WIDTH - 112, 30),
|
||||
Qt::AlignLeft | Qt::AlignVCenter,
|
||||
snapshot.runtime_active ? QObject::tr("OpenXR runtime active")
|
||||
: QObject::tr("Waiting for OpenXR runtime"));
|
||||
|
||||
painter.end();
|
||||
|
||||
std::vector<u8> pixels(static_cast<size_t>(image.sizeInBytes()));
|
||||
std::memcpy(pixels.data(), image.constBits(), pixels.size());
|
||||
return pixels;
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
OpenXRWiimoteConfigSessionController::OpenXRWiimoteConfigSessionController(MappingWindow* window,
|
||||
int port)
|
||||
: QObject(window), m_window(window), m_port(port), m_overlay_timer(new QTimer(this))
|
||||
{
|
||||
m_overlay_timer->setInterval(100);
|
||||
|
||||
connect(m_overlay_timer, &QTimer::timeout, this,
|
||||
&OpenXRWiimoteConfigSessionController::UpdateOverlay);
|
||||
connect(window, &QDialog::finished, this, &OpenXRWiimoteConfigSessionController::Stop);
|
||||
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
|
||||
[this](Core::State state) {
|
||||
if (state != Core::State::Uninitialized)
|
||||
Stop();
|
||||
});
|
||||
|
||||
QTimer::singleShot(0, this, &OpenXRWiimoteConfigSessionController::TryStart);
|
||||
}
|
||||
|
||||
OpenXRWiimoteConfigSessionController::~OpenXRWiimoteConfigSessionController()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void OpenXRWiimoteConfigSessionController::TryStart()
|
||||
{
|
||||
#if defined(ENABLE_VR) && defined(_WIN32)
|
||||
if (m_session)
|
||||
return;
|
||||
|
||||
if (Core::GetState(Core::System::GetInstance()) != Core::State::Uninitialized)
|
||||
return;
|
||||
|
||||
if (!Config::Get(Config::GFX_VR_ENABLE_OPENXR_CONFIG_SCENE))
|
||||
return;
|
||||
|
||||
if (Config::Get(Config::GetInfoForWiimoteSource(m_port)) != WiimoteSource::OpenXR)
|
||||
return;
|
||||
|
||||
if (VR::g_openxr)
|
||||
return;
|
||||
|
||||
m_session = std::make_unique<VR::OpenXRUtilitySession>();
|
||||
if (!m_session->Start())
|
||||
{
|
||||
m_session.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateOverlay();
|
||||
m_overlay_timer->start();
|
||||
#endif
|
||||
}
|
||||
|
||||
void OpenXRWiimoteConfigSessionController::Stop()
|
||||
{
|
||||
m_overlay_timer->stop();
|
||||
|
||||
#if defined(ENABLE_VR) && defined(_WIN32)
|
||||
if (m_session)
|
||||
{
|
||||
m_session->Stop();
|
||||
m_session.reset();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void OpenXRWiimoteConfigSessionController::UpdateOverlay()
|
||||
{
|
||||
#if defined(ENABLE_VR) && defined(_WIN32)
|
||||
if (!m_session)
|
||||
return;
|
||||
|
||||
if (!m_session->IsRunning())
|
||||
{
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const Common::VR::OpenXRInputSnapshot snapshot = Common::VR::OpenXRInputState::GetSnapshot();
|
||||
m_session->SetOverlayImage(RenderOverlayImage(m_port, snapshot), OVERLAY_WIDTH, OVERLAY_HEIGHT);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace VR
|
||||
{
|
||||
#if defined(ENABLE_VR) && defined(_WIN32)
|
||||
class OpenXRUtilitySession;
|
||||
#endif
|
||||
}
|
||||
|
||||
class MappingWindow;
|
||||
class QTimer;
|
||||
|
||||
class OpenXRWiimoteConfigSessionController final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OpenXRWiimoteConfigSessionController(MappingWindow* window, int port);
|
||||
~OpenXRWiimoteConfigSessionController() override;
|
||||
|
||||
private:
|
||||
void TryStart();
|
||||
void Stop();
|
||||
void UpdateOverlay();
|
||||
|
||||
MappingWindow* const m_window;
|
||||
const int m_port;
|
||||
QTimer* const m_overlay_timer;
|
||||
#if defined(ENABLE_VR) && defined(_WIN32)
|
||||
std::unique_ptr<VR::OpenXRUtilitySession> m_session;
|
||||
#endif
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Core/Config/WiimoteSettings.h"
|
||||
#include "Core/HW/Wiimote.h"
|
||||
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
|
||||
|
||||
@@ -22,14 +23,19 @@ void WiimoteEmuMotionControl::CreateMainLayout()
|
||||
{
|
||||
m_main_layout = new QHBoxLayout();
|
||||
|
||||
m_main_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Shake)));
|
||||
m_main_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Point)));
|
||||
m_main_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Tilt)));
|
||||
m_main_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Swing)));
|
||||
|
||||
const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(GetPort()));
|
||||
if (source != WiimoteSource::OpenXR)
|
||||
{
|
||||
m_main_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Shake)));
|
||||
m_main_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Tilt)));
|
||||
m_main_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::Swing)));
|
||||
}
|
||||
|
||||
setLayout(m_main_layout);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
|
||||
#include "DolphinQt/Config/Mapping/WiimoteEmuMotionControlIMU.h"
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Core/Config/WiimoteSettings.h"
|
||||
#include "Core/HW/Wiimote.h"
|
||||
#include "Core/HW/WiimoteEmu/WiimoteEmu.h"
|
||||
|
||||
@@ -27,34 +26,54 @@ WiimoteEmuMotionControlIMU::WiimoteEmuMotionControlIMU(MappingWindow* window)
|
||||
|
||||
void WiimoteEmuMotionControlIMU::CreateMainLayout()
|
||||
{
|
||||
auto* warning_layout = new QHBoxLayout();
|
||||
auto* warning_label =
|
||||
new QLabel(tr("The controls under Accelerometer and Gyroscope are designed to "
|
||||
"interface directly with motion sensor hardware. They are not intended for "
|
||||
"mapping traditional buttons, triggers or axes. You might need to configure "
|
||||
"alternate input sources before using these controls."));
|
||||
warning_label->setWordWrap(true);
|
||||
auto* warning_input_sources_button = new QPushButton(tr("Alternate Input Sources"));
|
||||
warning_layout->addWidget(
|
||||
QtUtils::CreateIconWarning(this, QStyle::SP_MessageBoxWarning, warning_label), 1);
|
||||
warning_layout->addWidget(warning_input_sources_button);
|
||||
connect(warning_input_sources_button, &QPushButton::clicked, this, [this] {
|
||||
ControllerInterfaceWindow window{this};
|
||||
window.exec();
|
||||
});
|
||||
const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(GetPort()));
|
||||
|
||||
auto* groups_layout = new QHBoxLayout();
|
||||
groups_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUPoint)));
|
||||
groups_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IRPassthrough)));
|
||||
groups_layout->addWidget(CreateGroupBox(
|
||||
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer)));
|
||||
groups_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope)));
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(),
|
||||
source == WiimoteSource::OpenXR ?
|
||||
WiimoteEmu::WiimoteGroup::Point :
|
||||
WiimoteEmu::WiimoteGroup::IMUPoint)));
|
||||
if (source != WiimoteSource::OpenXR)
|
||||
{
|
||||
groups_layout->addWidget(CreateGroupBox(
|
||||
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IRPassthrough)));
|
||||
groups_layout->addWidget(CreateGroupBox(
|
||||
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer)));
|
||||
groups_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope)));
|
||||
}
|
||||
else
|
||||
{
|
||||
auto* imu_layout = new QVBoxLayout();
|
||||
imu_layout->addWidget(
|
||||
CreateGroupBox(Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUGyroscope)));
|
||||
imu_layout->addWidget(CreateGroupBox(
|
||||
Wiimote::GetWiimoteGroup(GetPort(), WiimoteEmu::WiimoteGroup::IMUAccelerometer)));
|
||||
groups_layout->addLayout(imu_layout);
|
||||
}
|
||||
|
||||
m_main_layout = new QVBoxLayout();
|
||||
m_main_layout->addLayout(warning_layout);
|
||||
if (source != WiimoteSource::OpenXR)
|
||||
{
|
||||
auto* warning_layout = new QHBoxLayout();
|
||||
auto* warning_label =
|
||||
new QLabel(tr("The controls under Accelerometer and Gyroscope are designed to "
|
||||
"interface directly with motion sensor hardware. They are not intended for "
|
||||
"mapping traditional buttons, triggers or axes. You might need to configure "
|
||||
"alternate input sources before using these controls."));
|
||||
warning_label->setWordWrap(true);
|
||||
auto* warning_input_sources_button = new QPushButton(tr("Alternate Input Sources"));
|
||||
warning_layout->addWidget(
|
||||
QtUtils::CreateIconWarning(this, QStyle::SP_MessageBoxWarning, warning_label), 1);
|
||||
warning_layout->addWidget(warning_input_sources_button);
|
||||
connect(warning_input_sources_button, &QPushButton::clicked, this, [this] {
|
||||
ControllerInterfaceWindow window{this};
|
||||
window.exec();
|
||||
});
|
||||
|
||||
m_main_layout->addLayout(warning_layout);
|
||||
}
|
||||
m_main_layout->addLayout(groups_layout);
|
||||
|
||||
setLayout(m_main_layout);
|
||||
|
||||
@@ -13,12 +13,16 @@
|
||||
#include "DiscIO/Volume.h"
|
||||
|
||||
#include "DolphinQt/Config/ARCodeWidget.h"
|
||||
#include "DolphinQt/Config/ElementsGroupOverrideWidget.h"
|
||||
#include "DolphinQt/Config/FilesystemWidget.h"
|
||||
#include "DolphinQt/Config/GameConfigWidget.h"
|
||||
#include "DolphinQt/Config/GeckoCodeWidget.h"
|
||||
#include "DolphinQt/Config/GraphicsModListWidget.h"
|
||||
#include "DolphinQt/Config/VRConfigWidget.h"
|
||||
#include "DolphinQt/Config/InfoWidget.h"
|
||||
#include "DolphinQt/Config/PatchesWidget.h"
|
||||
#include "DolphinQt/Config/HideObjectWidget.h"
|
||||
#include "DolphinQt/Config/ShaderOverrideWidget.h"
|
||||
#include "DolphinQt/Config/VerifyWidget.h"
|
||||
#include "DolphinQt/QtUtils/WrapInScrollArea.h"
|
||||
|
||||
@@ -38,6 +42,7 @@ PropertiesDialog::PropertiesDialog(QWidget* parent, const UICommon::GameFile& ga
|
||||
new GeckoCodeWidget(game.GetGameID(), game.GetGameTDBID(), game.GetRevision());
|
||||
auto* const patches = new PatchesWidget(game);
|
||||
auto* const game_config = new GameConfigWidget(game);
|
||||
auto* const vr_config = new VRConfigWidget(game.GetGameID());
|
||||
auto* const graphics_mod_list = new GraphicsModListWidget(game);
|
||||
|
||||
connect(gecko, &GeckoCodeWidget::OpenGeneralSettings, this,
|
||||
@@ -59,11 +64,21 @@ PropertiesDialog::PropertiesDialog(QWidget* parent, const UICommon::GameFile& ga
|
||||
// Note: Intentional selective use of AddWrappedPane for a sensible dialog "minimumSize".
|
||||
AddWrappedPane(info, tr("Info"));
|
||||
AddPane(game_config, tr("Game Config"));
|
||||
AddPane(vr_config, tr("VR Config"));
|
||||
AddPane(patches, tr("Patches"));
|
||||
AddPane(ar, tr("AR Codes"));
|
||||
AddPane(gecko, tr("Gecko Codes"));
|
||||
AddWrappedPane(graphics_mod_list, tr("Graphics Mods"));
|
||||
|
||||
auto* const shader_overrides = new ShaderOverrideWidget(game.GetGameID());
|
||||
AddPane(shader_overrides, tr("Shader Overrides"));
|
||||
|
||||
auto* const element_group_overrides = new ElementsGroupOverrideWidget(game.GetGameID());
|
||||
AddPane(element_group_overrides, tr("Elements Group Overrides"));
|
||||
|
||||
auto* const hide_objects = new HideObjectWidget(game.GetGameID());
|
||||
AddPane(hide_objects, tr("Hide Objects"));
|
||||
|
||||
if (game.GetPlatform() != DiscIO::Platform::ELFOrDOL)
|
||||
{
|
||||
std::shared_ptr<DiscIO::Volume> volume = DiscIO::CreateVolume(game.GetFilePath());
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "DolphinQt/Settings/OnScreenDisplayPane.h"
|
||||
#include "DolphinQt/Settings/PathPane.h"
|
||||
#include "DolphinQt/Settings/TriforcePane.h"
|
||||
#include "DolphinQt/Settings/VRPane.h"
|
||||
#include "DolphinQt/Settings/WiiPane.h"
|
||||
|
||||
StackedSettingsWindow::StackedSettingsWindow(QWidget* parent) : QDialog{parent}
|
||||
@@ -193,6 +194,7 @@ SettingsWindow::SettingsWindow(MainWindow* parent) : StackedSettingsWindow{paren
|
||||
// If you change the order, don't forget to update the SettingsWindowPaneIndex enum.
|
||||
AddWrappedPane(new GeneralPane, tr("General"));
|
||||
AddPane(new GraphicsPane{parent, nullptr}, tr("Graphics"));
|
||||
AddWrappedPane(new VRPane, tr("VR"));
|
||||
AddWrappedPane(new ControllersPane, tr("Controllers"));
|
||||
AddWrappedPane(new InterfacePane, tr("Interface"));
|
||||
AddWrappedPane(new OnScreenDisplayPane, tr("On-Screen Display"));
|
||||
|
||||
@@ -42,6 +42,7 @@ enum class SettingsWindowPaneIndex : int
|
||||
{
|
||||
General = 0,
|
||||
Graphics,
|
||||
VR,
|
||||
Controllers,
|
||||
Interface,
|
||||
OnScreenDisplay,
|
||||
|
||||
@@ -0,0 +1,853 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/ShaderOverrideAddEditDialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QScrollArea>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSpinBox>
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Core/Config/GraphicsSettings.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "DolphinQt/Config/TextureHashBrowserDialog.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double UPM_OVERRIDE_MIN = 0.01;
|
||||
constexpr double UPM_OVERRIDE_MAX = 1000.0;
|
||||
constexpr double UPM_OVERRIDE_STEP = 0.01;
|
||||
constexpr int MAX_VISIBLE_TEXTURE_HASH_ROWS = 10;
|
||||
constexpr int TEXTURE_HASH_ROW_HEIGHT = 28;
|
||||
|
||||
} // namespace
|
||||
|
||||
ShaderOverrideAddEditDialog::ShaderOverrideAddEditDialog(
|
||||
QWidget* parent, const ShaderHunter::ShaderOverride* edit_override,
|
||||
const std::vector<std::string>& available_flags)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(edit_override ? tr("Edit Shader Override") : tr("Add Shader Override"));
|
||||
setMinimumWidth(350);
|
||||
|
||||
m_name_edit = new QLineEdit;
|
||||
m_name_edit->setPlaceholderText(tr("Override name..."));
|
||||
|
||||
m_comments_edit = new QPlainTextEdit;
|
||||
m_comments_edit->setPlaceholderText(tr("Optional notes..."));
|
||||
m_comments_edit->setTabChangesFocus(true);
|
||||
m_comments_edit->setMinimumHeight(70);
|
||||
|
||||
m_credits_edit = new QLineEdit;
|
||||
m_credits_edit->setPlaceholderText(tr("Optional author/credits..."));
|
||||
|
||||
m_hash_edit = new QLineEdit;
|
||||
m_hash_edit->setPlaceholderText(tr("Hex hash (e.g. 0012abcd)"));
|
||||
|
||||
m_type_combo = new QComboBox;
|
||||
m_type_combo->addItem(tr("Pixel Shader"), static_cast<int>(ShaderHunter::ShaderType::Pixel));
|
||||
m_type_combo->addItem(tr("Vertex Shader"), static_cast<int>(ShaderHunter::ShaderType::Vertex));
|
||||
m_type_combo->addItem(tr("Geometry Shader"),
|
||||
static_cast<int>(ShaderHunter::ShaderType::Geometry));
|
||||
|
||||
m_match_mode_label = new QLabel(tr("Match Mode:"));
|
||||
m_match_mode_combo = new QComboBox;
|
||||
m_match_mode_combo->addItem(tr("Exact Hash"),
|
||||
static_cast<int>(ShaderHunter::MatchMode::ExactHash));
|
||||
m_match_mode_combo->addItem(tr("Shader Family"),
|
||||
static_cast<int>(ShaderHunter::MatchMode::ShaderFamily));
|
||||
m_match_mode_combo->setToolTip(
|
||||
tr("Exact Hash: match only this exact shader hash.\n"
|
||||
"Shader Family: use the relaxed shader family signature."));
|
||||
|
||||
m_handling_combo = new QComboBox;
|
||||
m_handling_combo->addItem(tr("Skip"), static_cast<int>(ShaderHunter::HandlingType::Skip));
|
||||
m_handling_combo->addItem(tr("Screen"), static_cast<int>(ShaderHunter::HandlingType::Screen));
|
||||
m_handling_combo->addItem(tr("Fullscreen"),
|
||||
static_cast<int>(ShaderHunter::HandlingType::Fullscreen));
|
||||
m_handling_combo->addItem(tr("Head Locked"),
|
||||
static_cast<int>(ShaderHunter::HandlingType::HeadLocked));
|
||||
m_handling_combo->addItem(tr("Flag"), static_cast<int>(ShaderHunter::HandlingType::Flag));
|
||||
m_handling_combo->addItem(tr("Units per Meter"),
|
||||
static_cast<int>(ShaderHunter::HandlingType::UnitsPerMeter));
|
||||
|
||||
m_layer_label = new QLabel(tr("Layer:"));
|
||||
m_layer_spin = new QSpinBox;
|
||||
m_layer_spin->setRange(-1, 999);
|
||||
m_layer_spin->setSpecialValueText(tr("Auto"));
|
||||
m_layer_spin->setValue(-1);
|
||||
m_layer_spin->setToolTip(tr("Manual depth layer for Screen/Head Locked handling.\n"
|
||||
"-1 (Auto) = automatic per-draw counter.\n"
|
||||
"Higher values are closer to camera."));
|
||||
|
||||
m_element_depth_label = new QLabel(tr("Element Depth:"));
|
||||
m_element_depth_spin = new QDoubleSpinBox;
|
||||
m_element_depth_spin->setRange(-1.0, 0.01);
|
||||
m_element_depth_spin->setDecimals(4);
|
||||
m_element_depth_spin->setSingleStep(0.0001);
|
||||
m_element_depth_spin->setSpecialValueText(tr("Global"));
|
||||
m_element_depth_spin->setValue(-1.0);
|
||||
m_element_depth_spin->setToolTip(tr("Within-element depth range for this shader.\n"
|
||||
"-1 (Global) = use the global Element Depth setting.\n"
|
||||
"Higher values fix Z-fighting inside the element."));
|
||||
|
||||
m_units_per_meter_label = new QLabel(tr("Units per Meter:"));
|
||||
m_units_per_meter_spin = new QDoubleSpinBox;
|
||||
m_units_per_meter_spin->setRange(UPM_OVERRIDE_MIN, UPM_OVERRIDE_MAX);
|
||||
m_units_per_meter_spin->setDecimals(2);
|
||||
m_units_per_meter_spin->setSingleStep(UPM_OVERRIDE_STEP);
|
||||
m_units_per_meter_spin->setValue(1.0);
|
||||
m_units_per_meter_spin->setToolTip(tr("Temporary per-shader scale override for VR.\n"
|
||||
"Higher values make this shader appear larger."));
|
||||
|
||||
m_flag_label = new QLabel(tr("Flag Group:"));
|
||||
m_flag_edit = new QLineEdit;
|
||||
m_flag_edit->setPlaceholderText(tr("e.g. gameplay"));
|
||||
m_flag_edit->setToolTip(tr("When this shader is drawn, it sets this named flag.\n"
|
||||
"Other overrides can be conditional on this flag.\n"
|
||||
"Optional for non-Flag handling (shader acts as both override and flag)."));
|
||||
|
||||
m_condition_label = new QLabel(tr("Condition:"));
|
||||
m_condition_combo = new QComboBox;
|
||||
m_condition_combo->addItem(tr("(None)"), QString());
|
||||
for (const auto& flag : available_flags)
|
||||
m_condition_combo->addItem(QString::fromStdString(flag), QString::fromStdString(flag));
|
||||
m_condition_combo->setToolTip(
|
||||
tr("Select a flag condition for this override.\n"
|
||||
"Use Condition Mode to choose active or inactive behavior.\n"
|
||||
"Flags are detected from the current and previous frame."));
|
||||
|
||||
m_condition_mode_label = new QLabel(tr("Condition Mode:"));
|
||||
m_condition_mode_combo = new QComboBox;
|
||||
m_condition_mode_combo->addItem(tr("Activate"), false);
|
||||
m_condition_mode_combo->addItem(tr("Deactivate"), true);
|
||||
m_condition_mode_combo->setToolTip(
|
||||
tr("Activate: apply when the selected flag is active.\n"
|
||||
"Deactivate: apply when the selected flag is NOT active."));
|
||||
|
||||
m_clear_efb_check = new QCheckBox(tr("Clear EFB Copy"));
|
||||
m_clear_efb_check->setToolTip(
|
||||
tr("When this shader is drawn, the next EFB copy will be cleared to\n"
|
||||
"transparent instead of copying the framebuffer content.\n"
|
||||
"Use this to remove post-processing effects (bloom, blur) in VR.\n"
|
||||
"Can be combined with any handling type (Skip, Screen, etc.)."));
|
||||
|
||||
m_clear_efb_min_label = new QLabel(tr("EFB Min Width:"));
|
||||
m_clear_efb_min_spin = new QSpinBox;
|
||||
m_clear_efb_min_spin->setRange(0, 640);
|
||||
m_clear_efb_min_spin->setSingleStep(10);
|
||||
m_clear_efb_min_spin->setSpecialValueText(tr("Any"));
|
||||
m_clear_efb_min_spin->setValue(0);
|
||||
m_clear_efb_min_spin->setToolTip(tr("Only clear EFB copies whose native width >= this value.\n"
|
||||
"0 (Any) = no lower bound.\n"
|
||||
"Full EFB = 640 wide; set ~400 to only clear full-screen effects."));
|
||||
|
||||
m_clear_efb_max_label = new QLabel(tr("EFB Max Width:"));
|
||||
m_clear_efb_max_spin = new QSpinBox;
|
||||
m_clear_efb_max_spin->setRange(0, 640);
|
||||
m_clear_efb_max_spin->setSingleStep(10);
|
||||
m_clear_efb_max_spin->setSpecialValueText(tr("Any"));
|
||||
m_clear_efb_max_spin->setValue(0);
|
||||
m_clear_efb_max_spin->setToolTip(tr("Only clear EFB copies whose native width <= this value.\n"
|
||||
"0 (Any) = no upper bound.\n"
|
||||
"Use to restrict clearing to small or partial EFB copies."));
|
||||
|
||||
m_element_start_label = new QLabel(tr("Draw Call Start:"));
|
||||
m_element_start_spin = new QSpinBox;
|
||||
m_element_start_spin->setRange(0, 9999);
|
||||
m_element_start_spin->setValue(0);
|
||||
m_element_start_spin->setToolTip(
|
||||
tr("First draw call index to apply this override.\n"
|
||||
"Only used when Draw Call Filter is enabled.\n"
|
||||
"Use Draw Call mode in Shader Hunter to identify draw call indices."));
|
||||
|
||||
m_element_end_label = new QLabel(tr("Draw Call End:"));
|
||||
m_element_end_spin = new QSpinBox;
|
||||
m_element_end_spin->setRange(0, 9999);
|
||||
m_element_end_spin->setValue(0);
|
||||
m_element_end_spin->setToolTip(
|
||||
tr("Last draw call index to apply this override.\n"
|
||||
"Only used when Draw Call Filter is enabled.\n"
|
||||
"Range is inclusive: start=2, end=5 skips draws 2,3,4,5."));
|
||||
m_element_filter_check = new QCheckBox(tr("Draw Call Filter"));
|
||||
m_element_filter_check->setToolTip(
|
||||
tr("Enable to apply this override only to a specific draw-call range.\n"
|
||||
"Ranges adapt automatically when draw-call counts change between frames."));
|
||||
m_hash_family_check = new QCheckBox(tr("Match Shader Family"));
|
||||
m_hash_family_check->setToolTip(
|
||||
tr("Match this override against a relaxed shader family signature instead of the exact hash.\n"
|
||||
"Useful when the same effect changes hash across scenes or game revisions."));
|
||||
m_hash_family_check->hide();
|
||||
|
||||
m_view_textures_button = new QPushButton(tr("View Textures"));
|
||||
m_view_textures_button->setToolTip(
|
||||
tr("Show captured textures for this shader hash and type.\n"
|
||||
"Check textures in the popup and apply them to Texture Filters."));
|
||||
|
||||
m_texture_mode_label = new QLabel(tr("Texture Filter Mode:"));
|
||||
m_texture_mode_combo = new QComboBox;
|
||||
m_texture_mode_combo->addItem(tr("Include"), false);
|
||||
m_texture_mode_combo->addItem(tr("Exclude"), true);
|
||||
m_texture_mode_combo->setToolTip(
|
||||
tr("Include: apply override only when any listed texture hash is bound.\n"
|
||||
"Exclude: apply override only when none of the listed texture hashes are bound."));
|
||||
|
||||
m_texture_hash_container = new QWidget;
|
||||
m_texture_hash_layout = new QVBoxLayout(m_texture_hash_container);
|
||||
m_texture_hash_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_texture_hash_layout->setSpacing(4);
|
||||
m_texture_hash_scroll = new QScrollArea;
|
||||
m_texture_hash_scroll->setWidgetResizable(true);
|
||||
m_texture_hash_scroll->setFrameShape(QFrame::NoFrame);
|
||||
m_texture_hash_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_texture_hash_scroll->setWidget(m_texture_hash_container);
|
||||
AddTextureHashField(QString());
|
||||
|
||||
// Pre-fill fields in edit mode
|
||||
if (edit_override)
|
||||
{
|
||||
m_edit_element_reference_total = edit_override->element_reference_total;
|
||||
m_edit_family_signature = edit_override->family_signature;
|
||||
m_name_edit->setText(QString::fromStdString(edit_override->name));
|
||||
m_comments_edit->setPlainText(QString::fromStdString(edit_override->comments));
|
||||
m_credits_edit->setText(QString::fromStdString(edit_override->credits));
|
||||
m_hash_edit->setText(
|
||||
QString::fromStdString(fmt::format("{:016x}", edit_override->hash)));
|
||||
|
||||
const int type_idx = m_type_combo->findData(static_cast<int>(edit_override->type));
|
||||
if (type_idx >= 0)
|
||||
m_type_combo->setCurrentIndex(type_idx);
|
||||
|
||||
const ShaderHunter::MatchMode initial_match_mode =
|
||||
edit_override->match_mode == ShaderHunter::MatchMode::RuntimeElement ?
|
||||
ShaderHunter::MatchMode::ExactHash :
|
||||
edit_override->match_mode;
|
||||
const int match_mode_idx =
|
||||
m_match_mode_combo->findData(static_cast<int>(initial_match_mode));
|
||||
if (match_mode_idx >= 0)
|
||||
m_match_mode_combo->setCurrentIndex(match_mode_idx);
|
||||
|
||||
const int handling_idx = m_handling_combo->findData(static_cast<int>(edit_override->handling));
|
||||
if (handling_idx >= 0)
|
||||
m_handling_combo->setCurrentIndex(handling_idx);
|
||||
|
||||
m_layer_spin->setValue(edit_override->layer);
|
||||
m_element_depth_spin->setValue(edit_override->element_depth);
|
||||
if (edit_override->units_per_meter > 0.0f)
|
||||
m_units_per_meter_spin->setValue(edit_override->units_per_meter);
|
||||
m_flag_edit->setText(QString::fromStdString(edit_override->flag_group));
|
||||
|
||||
m_clear_efb_check->setChecked(edit_override->clear_efb);
|
||||
m_clear_efb_min_spin->setValue(edit_override->clear_efb_min_width);
|
||||
m_clear_efb_max_spin->setValue(edit_override->clear_efb_max_width);
|
||||
|
||||
const bool has_element_filter =
|
||||
edit_override->element_start >= 0 || edit_override->element_end >= 0;
|
||||
m_element_filter_check->setChecked(has_element_filter);
|
||||
m_hash_family_check->setChecked(edit_override->hash_family_match);
|
||||
m_element_start_spin->setValue(std::max(0, edit_override->element_start));
|
||||
m_element_end_spin->setValue(std::max(0, edit_override->element_end));
|
||||
|
||||
m_updating_texture_hash_fields = true;
|
||||
while (m_texture_hash_edits.size() < edit_override->texture_hashes.size())
|
||||
AddTextureHashField(QString());
|
||||
for (size_t i = 0; i < edit_override->texture_hashes.size(); i++)
|
||||
{
|
||||
m_texture_hash_edits[i]->setText(
|
||||
QString::fromStdString(fmt::format("{:016x}", edit_override->texture_hashes[i])));
|
||||
}
|
||||
for (size_t i = edit_override->texture_hashes.size(); i < m_texture_hash_edits.size(); i++)
|
||||
m_texture_hash_edits[i]->clear();
|
||||
m_updating_texture_hash_fields = false;
|
||||
EnsureTextureHashFieldRows();
|
||||
const int texture_mode_idx =
|
||||
m_texture_mode_combo->findData(edit_override->texture_hashes_excluded);
|
||||
if (texture_mode_idx >= 0)
|
||||
m_texture_mode_combo->setCurrentIndex(texture_mode_idx);
|
||||
|
||||
if (!edit_override->condition_flag.empty())
|
||||
{
|
||||
const int cond_idx =
|
||||
m_condition_combo->findData(QString::fromStdString(edit_override->condition_flag));
|
||||
if (cond_idx >= 0)
|
||||
m_condition_combo->setCurrentIndex(cond_idx);
|
||||
else
|
||||
{
|
||||
// Flag not in available list — add it dynamically
|
||||
m_condition_combo->addItem(QString::fromStdString(edit_override->condition_flag),
|
||||
QString::fromStdString(edit_override->condition_flag));
|
||||
m_condition_combo->setCurrentIndex(m_condition_combo->count() - 1);
|
||||
}
|
||||
}
|
||||
const int mode_idx = m_condition_mode_combo->findData(edit_override->condition_inverted);
|
||||
if (mode_idx >= 0)
|
||||
m_condition_mode_combo->setCurrentIndex(mode_idx);
|
||||
}
|
||||
|
||||
auto* form = new QFormLayout;
|
||||
form->addRow(tr("Name:"), m_name_edit);
|
||||
form->addRow(tr("Hash:"), m_hash_edit);
|
||||
form->addRow(tr("Shader Type:"), m_type_combo);
|
||||
form->addRow(m_match_mode_label, m_match_mode_combo);
|
||||
form->addRow(tr("Handling:"), m_handling_combo);
|
||||
form->addRow(m_layer_label, m_layer_spin);
|
||||
form->addRow(m_element_depth_label, m_element_depth_spin);
|
||||
form->addRow(m_units_per_meter_label, m_units_per_meter_spin);
|
||||
form->addRow(m_flag_label, m_flag_edit);
|
||||
form->addRow(m_condition_label, m_condition_combo);
|
||||
form->addRow(m_condition_mode_label, m_condition_mode_combo);
|
||||
form->addRow(QString(), m_clear_efb_check);
|
||||
form->addRow(m_clear_efb_min_label, m_clear_efb_min_spin);
|
||||
form->addRow(m_clear_efb_max_label, m_clear_efb_max_spin);
|
||||
form->addRow(QString(), m_element_filter_check);
|
||||
form->addRow(m_element_start_label, m_element_start_spin);
|
||||
form->addRow(m_element_end_label, m_element_end_spin);
|
||||
form->addRow(QString(), m_view_textures_button);
|
||||
form->addRow(m_texture_mode_label, m_texture_mode_combo);
|
||||
form->addRow(tr("Texture Filters:"), m_texture_hash_scroll);
|
||||
form->addRow(tr("Comments:"), m_comments_edit);
|
||||
form->addRow(tr("Credits:"), m_credits_edit);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &ShaderOverrideAddEditDialog::OnAccept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(m_handling_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&ShaderOverrideAddEditDialog::OnHandlingChanged);
|
||||
connect(m_clear_efb_check, &QCheckBox::toggled, this,
|
||||
&ShaderOverrideAddEditDialog::OnClearEFBChanged);
|
||||
connect(m_element_filter_check, &QCheckBox::toggled, this,
|
||||
&ShaderOverrideAddEditDialog::OnElementFilterChanged);
|
||||
connect(m_condition_combo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int) {
|
||||
const bool has_condition = !m_condition_combo->currentData().toString().isEmpty();
|
||||
m_condition_mode_label->setEnabled(has_condition);
|
||||
m_condition_mode_combo->setEnabled(has_condition);
|
||||
});
|
||||
connect(m_view_textures_button, &QPushButton::clicked, this,
|
||||
&ShaderOverrideAddEditDialog::ShowTextureBrowser);
|
||||
|
||||
auto* layout = new QVBoxLayout;
|
||||
layout->addLayout(form);
|
||||
layout->addWidget(buttons);
|
||||
setLayout(layout);
|
||||
|
||||
// Show/hide fields based on initial handling selection
|
||||
OnHandlingChanged();
|
||||
const bool has_condition = !m_condition_combo->currentData().toString().isEmpty();
|
||||
m_condition_mode_label->setEnabled(has_condition);
|
||||
m_condition_mode_combo->setEnabled(has_condition);
|
||||
OnClearEFBChanged();
|
||||
OnElementFilterChanged();
|
||||
}
|
||||
|
||||
ShaderHunter::ShaderOverride ShaderOverrideAddEditDialog::GetResult() const
|
||||
{
|
||||
ShaderHunter::ShaderOverride result;
|
||||
result.name = m_name_edit->text().toStdString();
|
||||
result.comments = m_comments_edit->toPlainText().trimmed().toStdString();
|
||||
result.credits = m_credits_edit->text().trimmed().toStdString();
|
||||
result.hash = std::strtoull(m_hash_edit->text().toStdString().c_str(), nullptr, 16);
|
||||
result.type =
|
||||
static_cast<ShaderHunter::ShaderType>(m_type_combo->currentData().toInt());
|
||||
result.match_mode =
|
||||
static_cast<ShaderHunter::MatchMode>(m_match_mode_combo->currentData().toInt());
|
||||
result.hash_family_match = false;
|
||||
result.family_signature = 0;
|
||||
|
||||
if (result.match_mode == ShaderHunter::MatchMode::ShaderFamily)
|
||||
{
|
||||
result.hash_family_match = true;
|
||||
result.family_signature = m_edit_family_signature;
|
||||
if (const auto signature =
|
||||
ShaderHunter::GetInstance().GetShaderFamilySignature(result.type, result.hash);
|
||||
signature.has_value())
|
||||
{
|
||||
result.family_signature = *signature;
|
||||
}
|
||||
}
|
||||
result.handling =
|
||||
static_cast<ShaderHunter::HandlingType>(m_handling_combo->currentData().toInt());
|
||||
result.layer = (result.handling == ShaderHunter::HandlingType::Screen ||
|
||||
result.handling == ShaderHunter::HandlingType::HeadLocked) ?
|
||||
m_layer_spin->value() : -1;
|
||||
result.element_depth = (result.handling == ShaderHunter::HandlingType::Screen ||
|
||||
result.handling == ShaderHunter::HandlingType::HeadLocked) ?
|
||||
static_cast<float>(m_element_depth_spin->value()) : -1.0f;
|
||||
result.units_per_meter = result.handling == ShaderHunter::HandlingType::UnitsPerMeter ?
|
||||
static_cast<float>(m_units_per_meter_spin->value()) :
|
||||
-1.0f;
|
||||
result.clear_efb = m_clear_efb_check->isChecked();
|
||||
result.clear_efb_min_width = result.clear_efb ? m_clear_efb_min_spin->value() : 0;
|
||||
result.clear_efb_max_width = result.clear_efb ? m_clear_efb_max_spin->value() : 0;
|
||||
if (m_element_filter_check->isChecked())
|
||||
{
|
||||
result.element_start = m_element_start_spin->value();
|
||||
result.element_end = m_element_end_spin->value();
|
||||
result.element_reference_total = m_edit_element_reference_total;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.element_start = -1;
|
||||
result.element_end = -1;
|
||||
result.element_reference_total = 0;
|
||||
}
|
||||
const auto texture_tokens = CollectTextureHashTokens();
|
||||
for (const std::string& token : texture_tokens)
|
||||
{
|
||||
const u64 parsed = std::strtoull(token.c_str(), nullptr, 16);
|
||||
if (parsed != 0)
|
||||
result.texture_hashes.push_back(parsed);
|
||||
}
|
||||
std::sort(result.texture_hashes.begin(), result.texture_hashes.end());
|
||||
result.texture_hashes.erase(
|
||||
std::unique(result.texture_hashes.begin(), result.texture_hashes.end()),
|
||||
result.texture_hashes.end());
|
||||
result.texture_hashes_excluded =
|
||||
!result.texture_hashes.empty() && m_texture_mode_combo->currentData().toBool();
|
||||
result.enabled = true;
|
||||
result.user_defined = true;
|
||||
|
||||
// flag_group is available on all handling types (optional except for Flag)
|
||||
result.flag_group = m_flag_edit->text().trimmed().toStdString();
|
||||
|
||||
if (result.handling != ShaderHunter::HandlingType::Flag)
|
||||
{
|
||||
result.condition_flag = m_condition_combo->currentData().toString().toStdString();
|
||||
result.condition_inverted =
|
||||
!result.condition_flag.empty() && m_condition_mode_combo->currentData().toBool();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::OnAccept()
|
||||
{
|
||||
const std::string name = m_name_edit->text().toStdString();
|
||||
if (name.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"), tr("Name cannot be empty."));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string hash_str = m_hash_edit->text().toStdString();
|
||||
if (hash_str.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"), tr("Hash cannot be empty."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate hex string
|
||||
for (char c : hash_str)
|
||||
{
|
||||
if (!std::isxdigit(static_cast<unsigned char>(c)))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"),
|
||||
tr("Hash must contain only hexadecimal characters (0-9, a-f)."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (hash_str.size() > 16)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"),
|
||||
tr("Hash must be at most 16 hex digits."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate texture hash hex string (if provided)
|
||||
const auto texture_tokens = CollectTextureHashTokens();
|
||||
for (const std::string& token : texture_tokens)
|
||||
{
|
||||
for (char c : token)
|
||||
{
|
||||
if (!std::isxdigit(static_cast<unsigned char>(c)))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"),
|
||||
tr("Texture filter values must contain only hexadecimal characters "
|
||||
"(0-9, a-f)."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (token.size() > 16)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"),
|
||||
tr("Each texture filter value must be at most 16 hex digits."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate flag group name for Flag handling
|
||||
if (m_element_filter_check->isChecked() &&
|
||||
m_element_start_spin->value() > m_element_end_spin->value())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"),
|
||||
tr("Draw Call Start must be less than or equal to Draw Call End."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate flag group name for Flag handling
|
||||
const auto handling = static_cast<ShaderHunter::HandlingType>(
|
||||
m_handling_combo->currentData().toInt());
|
||||
if (handling == ShaderHunter::HandlingType::Flag && m_flag_edit->text().trimmed().isEmpty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"),
|
||||
tr("Flag Group name cannot be empty for Flag handling."));
|
||||
return;
|
||||
}
|
||||
if (handling == ShaderHunter::HandlingType::UnitsPerMeter &&
|
||||
m_units_per_meter_spin->value() <= 0.0)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Validation Error"),
|
||||
tr("Units per Meter must be greater than 0."));
|
||||
return;
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::OnHandlingChanged()
|
||||
{
|
||||
const auto handling = static_cast<ShaderHunter::HandlingType>(
|
||||
m_handling_combo->currentData().toInt());
|
||||
const bool show_layer = (handling == ShaderHunter::HandlingType::Screen ||
|
||||
handling == ShaderHunter::HandlingType::HeadLocked);
|
||||
const bool show_units_per_meter = (handling == ShaderHunter::HandlingType::UnitsPerMeter);
|
||||
const bool is_flag = (handling == ShaderHunter::HandlingType::Flag);
|
||||
|
||||
m_layer_label->setVisible(show_layer);
|
||||
m_layer_spin->setVisible(show_layer);
|
||||
m_element_depth_label->setVisible(show_layer);
|
||||
m_element_depth_spin->setVisible(show_layer);
|
||||
m_units_per_meter_label->setVisible(show_units_per_meter);
|
||||
m_units_per_meter_spin->setVisible(show_units_per_meter);
|
||||
// Flag group is always visible (optional for non-Flag handling, required for Flag)
|
||||
m_flag_label->setVisible(true);
|
||||
m_flag_edit->setVisible(true);
|
||||
m_condition_label->setVisible(!is_flag);
|
||||
m_condition_combo->setVisible(!is_flag);
|
||||
m_condition_mode_label->setVisible(!is_flag);
|
||||
m_condition_mode_combo->setVisible(!is_flag);
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::OnClearEFBChanged()
|
||||
{
|
||||
const bool show = m_clear_efb_check->isChecked();
|
||||
m_clear_efb_min_label->setVisible(show);
|
||||
m_clear_efb_min_spin->setVisible(show);
|
||||
m_clear_efb_max_label->setVisible(show);
|
||||
m_clear_efb_max_spin->setVisible(show);
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::OnElementFilterChanged()
|
||||
{
|
||||
const bool show = m_element_filter_check->isChecked();
|
||||
m_element_start_label->setVisible(show);
|
||||
m_element_start_spin->setVisible(show);
|
||||
m_element_end_label->setVisible(show);
|
||||
m_element_end_spin->setVisible(show);
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::ShowTextureBrowser()
|
||||
{
|
||||
const std::string hash_str = m_hash_edit->text().trimmed().toStdString();
|
||||
if (hash_str.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("View Textures"),
|
||||
tr("Hash cannot be empty. Enter the shader hash first."));
|
||||
return;
|
||||
}
|
||||
if (hash_str.size() > 16)
|
||||
{
|
||||
QMessageBox::warning(this, tr("View Textures"),
|
||||
tr("Hash must be at most 16 hex digits."));
|
||||
return;
|
||||
}
|
||||
for (char c : hash_str)
|
||||
{
|
||||
if (!std::isxdigit(static_cast<unsigned char>(c)))
|
||||
{
|
||||
QMessageBox::warning(this, tr("View Textures"),
|
||||
tr("Hash must contain only hexadecimal characters (0-9, a-f)."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const u64 shader_hash = std::strtoull(hash_str.c_str(), nullptr, 16);
|
||||
const auto shader_type =
|
||||
static_cast<ShaderHunter::ShaderType>(m_type_combo->currentData().toInt());
|
||||
auto& hunter = ShaderHunter::GetInstance();
|
||||
const bool was_hunting_enabled = hunter.IsEnabled();
|
||||
const auto previous_active_type = hunter.GetActiveType();
|
||||
const int previous_selected_pos = hunter.GetSelectedPosition();
|
||||
const u64 previous_selected_hash = hunter.GetSelectedHash();
|
||||
|
||||
const char* type_str = shader_type == ShaderHunter::ShaderType::Pixel ? "PS" :
|
||||
shader_type == ShaderHunter::ShaderType::Vertex ? "VS" :
|
||||
"GS";
|
||||
|
||||
if (!was_hunting_enabled)
|
||||
hunter.SetEnabled(true);
|
||||
hunter.SetTextureToolActive(true);
|
||||
|
||||
const auto initial_hashes = CollectTextureHashValues();
|
||||
const auto sync_selected_hashes = [shader_hash, shader_type](const std::vector<u64>& hashes) {
|
||||
auto& sync_hunter = ShaderHunter::GetInstance();
|
||||
if (!sync_hunter.SelectShader(shader_type, shader_hash))
|
||||
return;
|
||||
|
||||
sync_hunter.ClearTextureSkipFilters();
|
||||
for (u64 selected_hash : hashes)
|
||||
sync_hunter.SetTextureSkipEnabled(selected_hash, true);
|
||||
};
|
||||
|
||||
TextureHashBrowserConfig browser_config;
|
||||
browser_config.title = tr("Textures for %1 %2")
|
||||
.arg(QString::fromLatin1(type_str))
|
||||
.arg(static_cast<qulonglong>(shader_hash), 16, 16,
|
||||
QLatin1Char('0'));
|
||||
browser_config.empty_info_text =
|
||||
tr("No textures captured yet for this shader.\n"
|
||||
"Shader Hunter was enabled temporarily for this window.");
|
||||
browser_config.current_label = tr("%1 %2")
|
||||
.arg(QString::fromLatin1(type_str))
|
||||
.arg(static_cast<qulonglong>(shader_hash), 16, 16,
|
||||
QLatin1Char('0'));
|
||||
browser_config.initial_selected_hashes = initial_hashes;
|
||||
browser_config.fetch_current_entries = [shader_hash, shader_type]() {
|
||||
auto& populate_hunter = ShaderHunter::GetInstance();
|
||||
if (!populate_hunter.SelectShader(shader_type, shader_hash))
|
||||
return std::vector<TextureHashBrowserEntry>{};
|
||||
|
||||
std::vector<TextureHashBrowserEntry> entries;
|
||||
for (const auto& tex : populate_hunter.GetTexturesForHash(shader_type, shader_hash))
|
||||
entries.push_back(TextureHashBrowserEntry{.hash = tex.hash, .name = tex.name});
|
||||
return entries;
|
||||
};
|
||||
browser_config.apply_selected_hashes = [this](const std::vector<u64>& hashes) {
|
||||
SetTextureHashFields(hashes);
|
||||
};
|
||||
browser_config.live_selection_changed = sync_selected_hashes;
|
||||
|
||||
auto* dlg = ShowTextureHashBrowserDialog(this, browser_config);
|
||||
|
||||
connect(dlg, &QObject::destroyed, this,
|
||||
[was_hunting_enabled, previous_active_type, previous_selected_pos,
|
||||
previous_selected_hash]() {
|
||||
auto& restore_hunter = ShaderHunter::GetInstance();
|
||||
restore_hunter.SetTextureToolActive(false);
|
||||
restore_hunter.ClearTextureSkipFilters();
|
||||
if (previous_selected_pos >= 0)
|
||||
{
|
||||
if (!restore_hunter.SelectShader(previous_active_type, previous_selected_hash))
|
||||
restore_hunter.SetActiveType(previous_active_type);
|
||||
}
|
||||
else
|
||||
{
|
||||
restore_hunter.SetActiveType(previous_active_type);
|
||||
}
|
||||
if (!was_hunting_enabled)
|
||||
restore_hunter.SetEnabled(false);
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::string> ShaderOverrideAddEditDialog::CollectTextureHashTokens() const
|
||||
{
|
||||
std::vector<std::string> tokens;
|
||||
const QRegularExpression separators(QStringLiteral("[,;\\s]+"));
|
||||
const QRegularExpression hex_exact(QStringLiteral("^[0-9A-Fa-f]{1,16}$"));
|
||||
const QRegularExpression hex_16_anywhere(QStringLiteral("([0-9A-Fa-f]{16})"));
|
||||
for (QLineEdit* edit : m_texture_hash_edits)
|
||||
{
|
||||
if (edit == nullptr)
|
||||
continue;
|
||||
const auto parts = edit->text().trimmed().split(separators, Qt::SkipEmptyParts);
|
||||
for (const QString& part : parts)
|
||||
{
|
||||
if (hex_exact.match(part).hasMatch())
|
||||
{
|
||||
tokens.push_back(part.toLower().toStdString());
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto match = hex_16_anywhere.match(part);
|
||||
if (match.hasMatch())
|
||||
{
|
||||
tokens.push_back(match.captured(1).toLower().toStdString());
|
||||
continue;
|
||||
}
|
||||
|
||||
tokens.push_back(part.toStdString());
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
std::vector<u64> ShaderOverrideAddEditDialog::CollectTextureHashValues() const
|
||||
{
|
||||
std::vector<u64> hashes;
|
||||
for (const std::string& token : CollectTextureHashTokens())
|
||||
{
|
||||
if (token.empty() || token.size() > 16)
|
||||
continue;
|
||||
bool valid = true;
|
||||
for (char c : token)
|
||||
{
|
||||
if (!std::isxdigit(static_cast<unsigned char>(c)))
|
||||
{
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!valid)
|
||||
continue;
|
||||
const u64 parsed = std::strtoull(token.c_str(), nullptr, 16);
|
||||
if (parsed != 0)
|
||||
hashes.push_back(parsed);
|
||||
}
|
||||
std::sort(hashes.begin(), hashes.end());
|
||||
hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end());
|
||||
return hashes;
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::SetTextureHashFields(const std::vector<u64>& hashes)
|
||||
{
|
||||
m_updating_texture_hash_fields = true;
|
||||
while (m_texture_hash_edits.size() < hashes.size())
|
||||
AddTextureHashField(QString());
|
||||
|
||||
for (size_t i = 0; i < hashes.size(); i++)
|
||||
{
|
||||
m_texture_hash_edits[i]->setText(
|
||||
QString::fromStdString(fmt::format("{:016x}", hashes[i])));
|
||||
}
|
||||
for (size_t i = hashes.size(); i < m_texture_hash_edits.size(); i++)
|
||||
m_texture_hash_edits[i]->clear();
|
||||
|
||||
m_updating_texture_hash_fields = false;
|
||||
EnsureTextureHashFieldRows();
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::AddTextureHashField(const QString& text)
|
||||
{
|
||||
auto* edit = new QLineEdit;
|
||||
edit->setPlaceholderText(tr("Hex hash"));
|
||||
edit->setToolTip(
|
||||
tr("Optional texture filter hash.\n"
|
||||
"Enter one hash per line; a new line is added automatically.\n"
|
||||
"You can also paste multiple hashes separated by comma, semicolon, or spaces."));
|
||||
edit->setText(text);
|
||||
m_texture_hash_layout->addWidget(edit);
|
||||
m_texture_hash_edits.push_back(edit);
|
||||
|
||||
connect(edit, &QLineEdit::textChanged, this, [this, edit](const QString&) {
|
||||
SanitizeTextureHashField(edit);
|
||||
EnsureTextureHashFieldRows();
|
||||
});
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::EnsureTextureHashFieldRows()
|
||||
{
|
||||
if (m_updating_texture_hash_fields)
|
||||
return;
|
||||
|
||||
m_updating_texture_hash_fields = true;
|
||||
|
||||
int last_non_empty = -1;
|
||||
for (int i = 0; i < static_cast<int>(m_texture_hash_edits.size()); i++)
|
||||
{
|
||||
if (!m_texture_hash_edits[i]->text().trimmed().isEmpty())
|
||||
last_non_empty = i;
|
||||
}
|
||||
|
||||
const int desired_rows = std::max(1, last_non_empty + 2);
|
||||
|
||||
while (static_cast<int>(m_texture_hash_edits.size()) < desired_rows)
|
||||
AddTextureHashField(QString());
|
||||
|
||||
while (static_cast<int>(m_texture_hash_edits.size()) > desired_rows)
|
||||
{
|
||||
QLineEdit* edit = m_texture_hash_edits.back();
|
||||
m_texture_hash_edits.pop_back();
|
||||
m_texture_hash_layout->removeWidget(edit);
|
||||
delete edit;
|
||||
}
|
||||
|
||||
const int visible_rows = std::min(desired_rows, MAX_VISIBLE_TEXTURE_HASH_ROWS);
|
||||
const int viewport_height = visible_rows * TEXTURE_HASH_ROW_HEIGHT;
|
||||
m_texture_hash_scroll->setMinimumHeight(viewport_height);
|
||||
m_texture_hash_scroll->setMaximumHeight(viewport_height);
|
||||
|
||||
m_updating_texture_hash_fields = false;
|
||||
}
|
||||
|
||||
void ShaderOverrideAddEditDialog::SanitizeTextureHashField(QLineEdit* edit)
|
||||
{
|
||||
if (edit == nullptr || m_updating_texture_hash_fields)
|
||||
return;
|
||||
|
||||
const QRegularExpression separators(QStringLiteral("[,;\\s]+"));
|
||||
const QRegularExpression hex_exact(QStringLiteral("^[0-9A-Fa-f]{1,16}$"));
|
||||
const QRegularExpression hex_16_anywhere(QStringLiteral("([0-9A-Fa-f]{16})"));
|
||||
|
||||
const auto parts = edit->text().trimmed().split(separators, Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty())
|
||||
return;
|
||||
|
||||
QStringList sanitized_parts;
|
||||
bool changed = false;
|
||||
for (const QString& part : parts)
|
||||
{
|
||||
if (hex_exact.match(part).hasMatch())
|
||||
{
|
||||
const QString lowered = part.toLower();
|
||||
sanitized_parts.push_back(lowered);
|
||||
if (lowered != part)
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto match = hex_16_anywhere.match(part);
|
||||
if (match.hasMatch())
|
||||
{
|
||||
sanitized_parts.push_back(match.captured(1).toLower());
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
sanitized_parts.push_back(part);
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
const QString new_text = sanitized_parts.join(QStringLiteral(" "));
|
||||
if (new_text == edit->text())
|
||||
return;
|
||||
|
||||
const QSignalBlocker blocker(edit);
|
||||
edit->setText(new_text);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "VideoCommon/ShaderHunter.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPlainTextEdit;
|
||||
class QPushButton;
|
||||
class QScrollArea;
|
||||
class QString;
|
||||
class QSpinBox;
|
||||
class QVBoxLayout;
|
||||
class QWidget;
|
||||
|
||||
class ShaderOverrideAddEditDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
// edit_override != nullptr → edit mode (pre-fills fields); nullptr → add mode.
|
||||
explicit ShaderOverrideAddEditDialog(
|
||||
QWidget* parent, const ShaderHunter::ShaderOverride* edit_override = nullptr,
|
||||
const std::vector<std::string>& available_flags = {});
|
||||
|
||||
ShaderHunter::ShaderOverride GetResult() const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> CollectTextureHashTokens() const;
|
||||
std::vector<u64> CollectTextureHashValues() const;
|
||||
void SetTextureHashFields(const std::vector<u64>& hashes);
|
||||
void AddTextureHashField(const QString& text);
|
||||
void EnsureTextureHashFieldRows();
|
||||
void SanitizeTextureHashField(QLineEdit* edit);
|
||||
void ShowTextureBrowser();
|
||||
|
||||
void OnAccept();
|
||||
void OnHandlingChanged();
|
||||
void OnClearEFBChanged();
|
||||
void OnElementFilterChanged();
|
||||
|
||||
QLineEdit* m_name_edit;
|
||||
QPlainTextEdit* m_comments_edit;
|
||||
QLineEdit* m_credits_edit;
|
||||
QLineEdit* m_hash_edit;
|
||||
QComboBox* m_type_combo;
|
||||
QComboBox* m_match_mode_combo;
|
||||
QLabel* m_match_mode_label;
|
||||
QComboBox* m_handling_combo;
|
||||
QSpinBox* m_layer_spin;
|
||||
QLabel* m_layer_label;
|
||||
QDoubleSpinBox* m_element_depth_spin;
|
||||
QLabel* m_element_depth_label;
|
||||
QDoubleSpinBox* m_units_per_meter_spin;
|
||||
QLabel* m_units_per_meter_label;
|
||||
QLineEdit* m_flag_edit;
|
||||
QLabel* m_flag_label;
|
||||
QComboBox* m_condition_combo;
|
||||
QLabel* m_condition_label;
|
||||
QComboBox* m_condition_mode_combo;
|
||||
QLabel* m_condition_mode_label;
|
||||
QCheckBox* m_clear_efb_check;
|
||||
QSpinBox* m_clear_efb_min_spin;
|
||||
QSpinBox* m_clear_efb_max_spin;
|
||||
QLabel* m_clear_efb_min_label;
|
||||
QLabel* m_clear_efb_max_label;
|
||||
QSpinBox* m_element_start_spin;
|
||||
QSpinBox* m_element_end_spin;
|
||||
QLabel* m_element_start_label;
|
||||
QLabel* m_element_end_label;
|
||||
QCheckBox* m_element_filter_check;
|
||||
QCheckBox* m_hash_family_check;
|
||||
QPushButton* m_view_textures_button;
|
||||
QComboBox* m_texture_mode_combo;
|
||||
QLabel* m_texture_mode_label;
|
||||
QWidget* m_texture_hash_container;
|
||||
QScrollArea* m_texture_hash_scroll;
|
||||
QVBoxLayout* m_texture_hash_layout;
|
||||
std::vector<QLineEdit*> m_texture_hash_edits;
|
||||
bool m_updating_texture_hash_fields = false;
|
||||
int m_edit_element_reference_total = 0;
|
||||
u64 m_edit_family_signature = 0;
|
||||
};
|
||||
@@ -0,0 +1,424 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/ShaderOverrideWidget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QColor>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QStringList>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "DolphinQt/Config/ShaderOverrideAddEditDialog.h"
|
||||
#include "DolphinQt/Debugger/ShaderHunterWidget.h"
|
||||
#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
|
||||
#include "DolphinQt/Settings.h"
|
||||
#include "VideoCommon/ShaderHunter.h"
|
||||
|
||||
ShaderOverrideWidget::ShaderOverrideWidget(std::string game_id)
|
||||
: m_game_id(std::move(game_id))
|
||||
{
|
||||
CreateWidgets();
|
||||
ConnectWidgets();
|
||||
LoadOverrides();
|
||||
}
|
||||
|
||||
ShaderOverrideWidget::~ShaderOverrideWidget() = default;
|
||||
|
||||
void ShaderOverrideWidget::CreateWidgets()
|
||||
{
|
||||
m_code_list = new QListWidget;
|
||||
|
||||
auto* info_label = new QLabel(
|
||||
tr("Shader overrides control how individual shaders are handled.\n"
|
||||
"Use Add/Edit to create overrides manually, or add them from the Shader Hunter tool.\n"
|
||||
"Handling: Skip = hide, Screen = world-fixed, Head Locked = follows head,\n"
|
||||
"Fullscreen = no VR.\n"
|
||||
"Units per Meter = temporary per-shader VR scale override.\n"
|
||||
"Flag = detect shader presence to conditionally enable other overrides.\n"
|
||||
"Clear EFB = clear the next EFB copy to transparent (removes post-processing effects)."));
|
||||
info_label->setWordWrap(true);
|
||||
|
||||
m_code_add = new NonDefaultQPushButton(tr("&Add Override"));
|
||||
m_code_edit = new NonDefaultQPushButton(tr("&Edit Override"));
|
||||
m_code_edit->setEnabled(false);
|
||||
m_code_remove = new NonDefaultQPushButton(tr("&Remove Override"));
|
||||
m_code_remove->setEnabled(false);
|
||||
|
||||
m_code_refresh = new NonDefaultQPushButton(tr("Re&fresh List"));
|
||||
m_code_reload = new NonDefaultQPushButton(tr("Reload &Shaders"));
|
||||
m_shader_hunter = new NonDefaultQPushButton(tr("Shader Hunter"));
|
||||
|
||||
auto* button_layout = new QHBoxLayout;
|
||||
button_layout->addWidget(m_shader_hunter);
|
||||
button_layout->addWidget(m_code_add);
|
||||
button_layout->addWidget(m_code_edit);
|
||||
button_layout->addWidget(m_code_remove);
|
||||
button_layout->addStretch();
|
||||
button_layout->addWidget(m_code_refresh);
|
||||
button_layout->addWidget(m_code_reload);
|
||||
|
||||
auto* layout = new QVBoxLayout{this};
|
||||
layout->addWidget(info_label);
|
||||
layout->addWidget(m_code_list);
|
||||
layout->addLayout(button_layout);
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::ConnectWidgets()
|
||||
{
|
||||
connect(m_code_list, &QListWidget::itemChanged, this, &ShaderOverrideWidget::OnItemChanged);
|
||||
connect(m_code_list, &QListWidget::itemSelectionChanged, this,
|
||||
&ShaderOverrideWidget::OnSelectionChanged);
|
||||
connect(m_code_add, &QPushButton::clicked, this, &ShaderOverrideWidget::OnAddClicked);
|
||||
connect(m_code_edit, &QPushButton::clicked, this, &ShaderOverrideWidget::OnEditClicked);
|
||||
connect(m_code_remove, &QPushButton::clicked, this, &ShaderOverrideWidget::OnRemoveClicked);
|
||||
connect(m_code_refresh, &QPushButton::clicked, this, &ShaderOverrideWidget::OnRefreshClicked);
|
||||
connect(m_code_reload, &QPushButton::clicked, this, &ShaderOverrideWidget::OnReloadClicked);
|
||||
connect(&Settings::Instance(), &Settings::ShaderOverridesChanged, this,
|
||||
[this] { LoadOverrides(); });
|
||||
connect(m_shader_hunter, &QPushButton::clicked, this, [this] {
|
||||
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);
|
||||
connect(m_shader_hunter_widget, &ShaderHunterWidget::OverridesChanged, this,
|
||||
[this] { LoadOverrides(); });
|
||||
m_shader_hunter_widget->show();
|
||||
connect(m_shader_hunter_widget, &QObject::destroyed, this,
|
||||
[this] { m_shader_hunter_widget = nullptr; });
|
||||
});
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::LoadOverrides()
|
||||
{
|
||||
m_overrides = ShaderHunter::LoadOverridesFromINI(m_game_id);
|
||||
|
||||
m_code_list->setEnabled(!m_game_id.empty());
|
||||
m_code_remove->setEnabled(false);
|
||||
m_code_edit->setEnabled(false);
|
||||
|
||||
UpdateList();
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::SaveOverrides()
|
||||
{
|
||||
ShaderHunter::SaveOverridesToINI(m_game_id, m_overrides);
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::UpdateList()
|
||||
{
|
||||
// Block signals to prevent itemChanged → SaveOverrides() during list construction.
|
||||
const QSignalBlocker blocker(m_code_list);
|
||||
m_code_list->clear();
|
||||
|
||||
struct HandlingHashKey
|
||||
{
|
||||
int handling = 0;
|
||||
u64 hash = 0;
|
||||
|
||||
bool operator==(const HandlingHashKey& other) const
|
||||
{
|
||||
return handling == other.handling && hash == other.hash;
|
||||
}
|
||||
};
|
||||
struct HandlingHashKeyHasher
|
||||
{
|
||||
size_t operator()(const HandlingHashKey& key) const noexcept
|
||||
{
|
||||
return std::hash<u64>{}(key.hash) ^ (std::hash<int>{}(key.handling) << 1);
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<u64, size_t> hash_counts;
|
||||
std::unordered_map<HandlingHashKey, size_t, HandlingHashKeyHasher> handling_hash_counts;
|
||||
hash_counts.reserve(m_overrides.size());
|
||||
handling_hash_counts.reserve(m_overrides.size());
|
||||
for (const auto& ovr : m_overrides)
|
||||
{
|
||||
++hash_counts[ovr.hash];
|
||||
++handling_hash_counts[{static_cast<int>(ovr.handling), ovr.hash}];
|
||||
}
|
||||
|
||||
// Build sorted display order: Flag overrides first, then their conditionals, then standalone.
|
||||
std::vector<size_t> sorted_indices;
|
||||
sorted_indices.reserve(m_overrides.size());
|
||||
|
||||
// 1. Overrides that set a flag (Flag-only or combined), then their conditionals
|
||||
for (size_t i = 0; i < m_overrides.size(); i++)
|
||||
{
|
||||
if (!m_overrides[i].flag_group.empty())
|
||||
{
|
||||
sorted_indices.push_back(i);
|
||||
// 2. Conditionals that depend on this flag
|
||||
const auto& flag = m_overrides[i].flag_group;
|
||||
for (size_t j = 0; j < m_overrides.size(); j++)
|
||||
{
|
||||
if (j != i && m_overrides[j].condition_flag == flag &&
|
||||
std::find(sorted_indices.begin(), sorted_indices.end(), j) == sorted_indices.end())
|
||||
sorted_indices.push_back(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. Standalone overrides (no flag_group, no condition)
|
||||
for (size_t i = 0; i < m_overrides.size(); i++)
|
||||
{
|
||||
if (m_overrides[i].flag_group.empty() && m_overrides[i].condition_flag.empty() &&
|
||||
std::find(sorted_indices.begin(), sorted_indices.end(), i) == sorted_indices.end())
|
||||
{
|
||||
sorted_indices.push_back(i);
|
||||
}
|
||||
}
|
||||
// 4. Any conditional overrides whose flag doesn't exist (orphaned)
|
||||
for (size_t i = 0; i < m_overrides.size(); i++)
|
||||
{
|
||||
if (!m_overrides[i].condition_flag.empty() &&
|
||||
std::find(sorted_indices.begin(), sorted_indices.end(), i) == sorted_indices.end())
|
||||
{
|
||||
sorted_indices.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t idx : sorted_indices)
|
||||
{
|
||||
const auto& ovr = m_overrides[idx];
|
||||
|
||||
const char* type_str = ovr.type == ShaderHunter::ShaderType::Vertex ? "VS" :
|
||||
ovr.type == ShaderHunter::ShaderType::Geometry ? "GS" :
|
||||
"PS";
|
||||
const char* handling_str = ovr.handling == ShaderHunter::HandlingType::Screen ? "screen" :
|
||||
ovr.handling == ShaderHunter::HandlingType::Fullscreen ||
|
||||
ovr.handling == ShaderHunter::HandlingType::FullscreenMono ?
|
||||
"fullscreen" :
|
||||
ovr.handling == ShaderHunter::HandlingType::HeadLocked ? "headlocked" :
|
||||
ovr.handling == ShaderHunter::HandlingType::Flag ? "flag" :
|
||||
ovr.handling == ShaderHunter::HandlingType::UnitsPerMeter ?
|
||||
"units_per_meter" :
|
||||
"skip";
|
||||
|
||||
QString label;
|
||||
if (ovr.handling == ShaderHunter::HandlingType::Flag)
|
||||
{
|
||||
label = QStringLiteral("%1 [%2 %3] (flag -> '%4')")
|
||||
.arg(QString::fromStdString(ovr.name))
|
||||
.arg(QString::fromLatin1(type_str))
|
||||
.arg(ovr.hash, 16, 16, QLatin1Char('0'))
|
||||
.arg(QString::fromStdString(ovr.flag_group));
|
||||
}
|
||||
else if (!ovr.flag_group.empty())
|
||||
{
|
||||
// Non-Flag override that also sets a flag
|
||||
label = QStringLiteral("%1 [%2 %3] (%4, flag -> '%5')")
|
||||
.arg(QString::fromStdString(ovr.name))
|
||||
.arg(QString::fromLatin1(type_str))
|
||||
.arg(ovr.hash, 16, 16, QLatin1Char('0'))
|
||||
.arg(QString::fromLatin1(handling_str))
|
||||
.arg(QString::fromStdString(ovr.flag_group));
|
||||
}
|
||||
else
|
||||
{
|
||||
label = QStringLiteral("%1 [%2 %3] (%4)")
|
||||
.arg(QString::fromStdString(ovr.name))
|
||||
.arg(QString::fromLatin1(type_str))
|
||||
.arg(ovr.hash, 16, 16, QLatin1Char('0'))
|
||||
.arg(QString::fromLatin1(handling_str));
|
||||
}
|
||||
|
||||
if (ovr.handling == ShaderHunter::HandlingType::Screen ||
|
||||
ovr.handling == ShaderHunter::HandlingType::HeadLocked)
|
||||
{
|
||||
if (ovr.layer >= 0)
|
||||
label += QStringLiteral(" L%1").arg(ovr.layer);
|
||||
if (ovr.element_depth >= 0.0f)
|
||||
label += QStringLiteral(" D%1").arg(ovr.element_depth, 0, 'f', 4);
|
||||
}
|
||||
else if (ovr.handling == ShaderHunter::HandlingType::UnitsPerMeter && ovr.units_per_meter > 0.0f)
|
||||
{
|
||||
label += QStringLiteral(" UPM%1").arg(ovr.units_per_meter, 0, 'f', 2);
|
||||
}
|
||||
|
||||
if (ovr.hash_family_match)
|
||||
label += QStringLiteral(" family");
|
||||
|
||||
if (ovr.clear_efb)
|
||||
{
|
||||
if (ovr.clear_efb_min_width > 0 || ovr.clear_efb_max_width > 0)
|
||||
label += QStringLiteral(" +clearEFB(%1-%2)")
|
||||
.arg(ovr.clear_efb_min_width)
|
||||
.arg(ovr.clear_efb_max_width > 0 ? ovr.clear_efb_max_width : 640);
|
||||
else
|
||||
label += QStringLiteral(" +clearEFB");
|
||||
}
|
||||
|
||||
if (ovr.element_start >= 0 && ovr.element_end >= 0)
|
||||
{
|
||||
label += QStringLiteral(" elem[%1-%2]").arg(ovr.element_start).arg(ovr.element_end);
|
||||
if (ovr.element_reference_total > 0)
|
||||
label += QStringLiteral("@%1").arg(ovr.element_reference_total);
|
||||
}
|
||||
|
||||
if (!ovr.texture_hashes.empty())
|
||||
{
|
||||
QStringList texture_hashes;
|
||||
for (u64 texture_hash : ovr.texture_hashes)
|
||||
{
|
||||
texture_hashes.append(
|
||||
QString::number(texture_hash, 16).rightJustified(16, QLatin1Char('0')));
|
||||
}
|
||||
label += QStringLiteral(" tex(%1):%2")
|
||||
.arg(ovr.texture_hashes_excluded ? QStringLiteral("exclude") :
|
||||
QStringLiteral("include"))
|
||||
.arg(texture_hashes.join(QStringLiteral(",")));
|
||||
}
|
||||
|
||||
if (!ovr.condition_flag.empty())
|
||||
{
|
||||
const QString condition_text = ovr.condition_inverted ?
|
||||
QStringLiteral("[when not '%1']").arg(
|
||||
QString::fromStdString(ovr.condition_flag)) :
|
||||
QStringLiteral("[when '%1']").arg(
|
||||
QString::fromStdString(ovr.condition_flag));
|
||||
label = QStringLiteral(" \u2514 ") + label + QStringLiteral(" ") + condition_text;
|
||||
}
|
||||
|
||||
const bool has_duplicate_handling_hash =
|
||||
handling_hash_counts[{static_cast<int>(ovr.handling), ovr.hash}] > 1;
|
||||
const bool has_duplicate_hash = hash_counts[ovr.hash] > 1;
|
||||
const bool has_hash_only_duplicate = has_duplicate_hash && !has_duplicate_handling_hash;
|
||||
if (has_duplicate_handling_hash)
|
||||
label += QStringLiteral(" [DUPLICATE HANDLING+HASH]");
|
||||
else if (has_hash_only_duplicate)
|
||||
label += QStringLiteral(" [DUPLICATE HASH]");
|
||||
|
||||
auto* item = new QListWidgetItem(label);
|
||||
|
||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(ovr.enabled ? Qt::Checked : Qt::Unchecked);
|
||||
item->setData(Qt::UserRole, static_cast<int>(idx));
|
||||
if (has_duplicate_handling_hash)
|
||||
{
|
||||
item->setForeground(QColor(220, 85, 85));
|
||||
item->setToolTip(
|
||||
tr("Duplicate Handling+Hash: another override in this list has the same handling mode and hash."));
|
||||
}
|
||||
else if (has_hash_only_duplicate)
|
||||
{
|
||||
item->setForeground(QColor(214, 160, 0));
|
||||
item->setToolTip(
|
||||
tr("Duplicate Hash: another override in this list has the same hash but a different shader "
|
||||
"type."));
|
||||
}
|
||||
|
||||
m_code_list->addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::OnItemChanged(QListWidgetItem* item)
|
||||
{
|
||||
const int idx = item->data(Qt::UserRole).toInt();
|
||||
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
|
||||
return;
|
||||
|
||||
m_overrides[idx].enabled = (item->checkState() == Qt::Checked);
|
||||
SaveOverrides();
|
||||
ShaderHunter::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::OnSelectionChanged()
|
||||
{
|
||||
const auto items = m_code_list->selectedItems();
|
||||
const bool has_selection = !items.empty();
|
||||
m_code_remove->setEnabled(has_selection);
|
||||
m_code_edit->setEnabled(has_selection);
|
||||
}
|
||||
|
||||
std::vector<std::string> ShaderOverrideWidget::CollectAvailableFlags() const
|
||||
{
|
||||
std::vector<std::string> flags;
|
||||
for (const auto& ovr : m_overrides)
|
||||
{
|
||||
if (!ovr.flag_group.empty())
|
||||
{
|
||||
if (std::find(flags.begin(), flags.end(), ovr.flag_group) == flags.end())
|
||||
flags.push_back(ovr.flag_group);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::OnAddClicked()
|
||||
{
|
||||
ShaderOverrideAddEditDialog dialog(this, nullptr, CollectAvailableFlags());
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
m_overrides.push_back(dialog.GetResult());
|
||||
SaveOverrides();
|
||||
UpdateList();
|
||||
ShaderHunter::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::OnEditClicked()
|
||||
{
|
||||
const auto items = m_code_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
|
||||
const int idx = items[0]->data(Qt::UserRole).toInt();
|
||||
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
|
||||
return;
|
||||
|
||||
ShaderOverrideAddEditDialog dialog(this, &m_overrides[idx], CollectAvailableFlags());
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
auto result = dialog.GetResult();
|
||||
result.enabled = m_overrides[idx].enabled; // Preserve enabled state
|
||||
m_overrides[idx] = result;
|
||||
SaveOverrides();
|
||||
UpdateList();
|
||||
ShaderHunter::GetInstance().LoadOverrides(m_game_id);
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::OnRemoveClicked()
|
||||
{
|
||||
const auto items = m_code_list->selectedItems();
|
||||
if (items.empty())
|
||||
return;
|
||||
|
||||
const int idx = items[0]->data(Qt::UserRole).toInt();
|
||||
if (idx < 0 || idx >= static_cast<int>(m_overrides.size()))
|
||||
return;
|
||||
|
||||
m_overrides.erase(m_overrides.begin() + idx);
|
||||
|
||||
SaveOverrides();
|
||||
UpdateList();
|
||||
ShaderHunter::GetInstance().LoadOverrides(m_game_id);
|
||||
|
||||
m_code_remove->setEnabled(false);
|
||||
m_code_edit->setEnabled(false);
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::OnRefreshClicked()
|
||||
{
|
||||
// Re-read overrides from the INI file and refresh the list display
|
||||
LoadOverrides();
|
||||
}
|
||||
|
||||
void ShaderOverrideWidget::OnReloadClicked()
|
||||
{
|
||||
// Force the runtime ShaderHunter to reload overrides from the INI,
|
||||
// so enabled/disabled changes take effect in the running game without restarting.
|
||||
auto& hunter = ShaderHunter::GetInstance();
|
||||
hunter.LoadOverrides(m_game_id);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPointer>
|
||||
#include <QWidget>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "VideoCommon/ShaderHunter.h"
|
||||
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QPushButton;
|
||||
class ShaderHunterWidget;
|
||||
|
||||
class ShaderOverrideWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ShaderOverrideWidget(std::string game_id);
|
||||
~ShaderOverrideWidget() override;
|
||||
|
||||
private:
|
||||
void CreateWidgets();
|
||||
void ConnectWidgets();
|
||||
void UpdateList();
|
||||
void LoadOverrides();
|
||||
void SaveOverrides();
|
||||
|
||||
void OnItemChanged(QListWidgetItem* item);
|
||||
void OnSelectionChanged();
|
||||
void OnAddClicked();
|
||||
void OnEditClicked();
|
||||
void OnRemoveClicked();
|
||||
void OnRefreshClicked();
|
||||
void OnReloadClicked();
|
||||
std::vector<std::string> CollectAvailableFlags() const;
|
||||
|
||||
std::string m_game_id;
|
||||
|
||||
QListWidget* m_code_list;
|
||||
QPushButton* m_code_add;
|
||||
QPushButton* m_code_edit;
|
||||
QPushButton* m_code_remove;
|
||||
QPushButton* m_code_refresh;
|
||||
QPushButton* m_code_reload;
|
||||
QPushButton* m_shader_hunter;
|
||||
QPointer<ShaderHunterWidget> m_shader_hunter_widget;
|
||||
|
||||
std::vector<ShaderHunter::ShaderOverride> m_overrides;
|
||||
};
|
||||
@@ -0,0 +1,335 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/TextureHashBrowserDialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QBrush>
|
||||
#include <QCheckBox>
|
||||
#include <QColor>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QImageReader>
|
||||
#include <QLabel>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSize>
|
||||
#include <QTimer>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
QString ToHashHex(u64 hash)
|
||||
{
|
||||
return QStringLiteral("%1").arg(static_cast<qulonglong>(hash), 16, 16, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
std::unordered_map<u64, QString> FindTexturePreviewPaths(const std::vector<u64>& hashes)
|
||||
{
|
||||
std::unordered_map<u64, QString> result;
|
||||
if (hashes.empty())
|
||||
return result;
|
||||
|
||||
std::unordered_set<u64> remaining(hashes.begin(), hashes.end());
|
||||
std::unordered_map<u64, QString> patterns;
|
||||
patterns.reserve(remaining.size());
|
||||
for (u64 hash : remaining)
|
||||
patterns.emplace(hash, QStringLiteral("_%1_").arg(ToHashHex(hash)));
|
||||
|
||||
const QString dump_root = QString::fromStdString(File::GetUserPath(D_DUMPTEXTURES_IDX));
|
||||
QStringList roots;
|
||||
const std::string game_id = SConfig::GetInstance().GetGameID();
|
||||
if (!game_id.empty())
|
||||
roots.push_back(QDir::cleanPath(dump_root + QString::fromStdString(game_id)));
|
||||
roots.push_back(QDir::cleanPath(dump_root));
|
||||
|
||||
for (const QString& root : roots)
|
||||
{
|
||||
if (!QDir(root).exists())
|
||||
continue;
|
||||
|
||||
QDirIterator it(root, QDir::Files, QDirIterator::Subdirectories);
|
||||
while (it.hasNext() && !remaining.empty())
|
||||
{
|
||||
const QString path = it.next();
|
||||
const QFileInfo info(path);
|
||||
const QString suffix = info.suffix().toLower();
|
||||
if (suffix != QStringLiteral("png") && suffix != QStringLiteral("jpg") &&
|
||||
suffix != QStringLiteral("jpeg") && suffix != QStringLiteral("bmp") &&
|
||||
suffix != QStringLiteral("tga"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString filename = info.fileName().toLower();
|
||||
for (auto iter = remaining.begin(); iter != remaining.end();)
|
||||
{
|
||||
const u64 hash = *iter;
|
||||
if (filename.contains(patterns[hash]))
|
||||
{
|
||||
result.emplace(hash, path);
|
||||
iter = remaining.erase(iter);
|
||||
}
|
||||
else
|
||||
{
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining.empty())
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QPixmap LoadPreviewPixmapFresh(const QString& path)
|
||||
{
|
||||
QImageReader reader(path);
|
||||
reader.setAutoTransform(true);
|
||||
const QImage image = reader.read();
|
||||
if (image.isNull())
|
||||
return {};
|
||||
return QPixmap::fromImage(image);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QDialog* ShowTextureHashBrowserDialog(QWidget* parent, const TextureHashBrowserConfig& config)
|
||||
{
|
||||
auto* dlg = new QDialog(parent);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dlg->setWindowTitle(config.title);
|
||||
dlg->setMinimumSize(620, 320);
|
||||
|
||||
auto* info = new QLabel;
|
||||
info->setWordWrap(true);
|
||||
|
||||
auto* continuous_scan_check = new QCheckBox(QObject::tr("Continuous Scan"));
|
||||
continuous_scan_check->setToolTip(
|
||||
QObject::tr("Continuously refresh while this window is open.\n"
|
||||
"Textures seen during scan stay visible in gray."));
|
||||
|
||||
auto* scan_timer = new QTimer(dlg);
|
||||
scan_timer->setInterval(250);
|
||||
|
||||
auto* tree = new QTreeWidget;
|
||||
tree->setHeaderLabels({QObject::tr("Use"), QObject::tr("Texture Hash"), QObject::tr("Name"),
|
||||
QObject::tr("Preview")});
|
||||
tree->setRootIsDecorated(false);
|
||||
tree->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
tree->header()->setStretchLastSection(true);
|
||||
tree->setIconSize(QSize(64, 64));
|
||||
|
||||
auto selected_hashes = std::make_shared<std::unordered_set<u64>>(
|
||||
config.initial_selected_hashes.begin(), config.initial_selected_hashes.end());
|
||||
auto scanned_hashes = std::make_shared<std::unordered_map<u64, std::string>>();
|
||||
|
||||
const auto notify_live_selection = [selected_hashes, config]() {
|
||||
if (!config.live_selection_changed)
|
||||
return;
|
||||
|
||||
std::vector<u64> hashes(selected_hashes->begin(), selected_hashes->end());
|
||||
std::sort(hashes.begin(), hashes.end());
|
||||
config.live_selection_changed(hashes);
|
||||
};
|
||||
|
||||
const auto populate = [tree, info, selected_hashes, scanned_hashes, continuous_scan_check,
|
||||
config, notify_live_selection]() {
|
||||
notify_live_selection();
|
||||
const QString current_label =
|
||||
config.fetch_current_label ? config.fetch_current_label() : config.current_label;
|
||||
|
||||
const auto textures = config.fetch_current_entries ? config.fetch_current_entries() :
|
||||
std::vector<TextureHashBrowserEntry>{};
|
||||
|
||||
std::unordered_map<u64, TextureHashBrowserEntry> captured;
|
||||
captured.reserve(textures.size());
|
||||
for (const auto& tex : textures)
|
||||
{
|
||||
captured.emplace(tex.hash, tex);
|
||||
if (continuous_scan_check->isChecked())
|
||||
{
|
||||
auto& saved_name = (*scanned_hashes)[tex.hash];
|
||||
if (saved_name.empty() && !tex.name.empty())
|
||||
saved_name = tex.name;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<u64> texture_hashes;
|
||||
texture_hashes.reserve(textures.size() + selected_hashes->size() + scanned_hashes->size());
|
||||
for (const auto& [captured_hash, _] : captured)
|
||||
texture_hashes.push_back(captured_hash);
|
||||
for (u64 saved_hash : *selected_hashes)
|
||||
texture_hashes.push_back(saved_hash);
|
||||
for (const auto& [seen_hash, _] : *scanned_hashes)
|
||||
texture_hashes.push_back(seen_hash);
|
||||
std::sort(texture_hashes.begin(), texture_hashes.end());
|
||||
texture_hashes.erase(std::unique(texture_hashes.begin(), texture_hashes.end()),
|
||||
texture_hashes.end());
|
||||
|
||||
const auto preview_paths = FindTexturePreviewPaths(texture_hashes);
|
||||
|
||||
const QSignalBlocker blocker(tree);
|
||||
tree->clear();
|
||||
|
||||
int saved_only_count = 0;
|
||||
int scanned_only_count = 0;
|
||||
for (u64 hash : texture_hashes)
|
||||
{
|
||||
const auto captured_it = captured.find(hash);
|
||||
const bool in_current_scene = (captured_it != captured.end());
|
||||
const bool is_saved_only = !in_current_scene && selected_hashes->count(hash) > 0;
|
||||
if (is_saved_only)
|
||||
++saved_only_count;
|
||||
const bool is_scanned_only = !in_current_scene && !is_saved_only &&
|
||||
scanned_hashes->find(hash) != scanned_hashes->end();
|
||||
if (is_scanned_only)
|
||||
++scanned_only_count;
|
||||
|
||||
auto* item = new QTreeWidgetItem;
|
||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(0, selected_hashes->count(hash) > 0 ? Qt::Checked : Qt::Unchecked);
|
||||
item->setText(1, ToHashHex(hash));
|
||||
|
||||
if (in_current_scene)
|
||||
{
|
||||
const auto& tex = captured_it->second;
|
||||
item->setText(2, tex.name.empty() ? QObject::tr("(unknown)") :
|
||||
QString::fromStdString(tex.name));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (is_saved_only)
|
||||
{
|
||||
item->setText(2, QObject::tr("(saved filter, not in current selection)"));
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto scanned_it = scanned_hashes->find(hash);
|
||||
const bool has_scanned_name =
|
||||
scanned_it != scanned_hashes->end() && !scanned_it->second.empty();
|
||||
item->setText(2, has_scanned_name ? QString::fromStdString(scanned_it->second) :
|
||||
QObject::tr("(seen during scan, not in current selection)"));
|
||||
}
|
||||
const QBrush gray_brush(QColor(140, 140, 140));
|
||||
item->setForeground(1, gray_brush);
|
||||
item->setForeground(2, gray_brush);
|
||||
}
|
||||
|
||||
item->setData(0, Qt::UserRole, static_cast<qulonglong>(hash));
|
||||
|
||||
const auto preview_it = preview_paths.find(hash);
|
||||
if (preview_it != preview_paths.end())
|
||||
{
|
||||
QPixmap pixmap = LoadPreviewPixmapFresh(preview_it->second);
|
||||
if (!pixmap.isNull())
|
||||
{
|
||||
item->setData(3, Qt::DecorationRole,
|
||||
pixmap.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
item->setSizeHint(3, QSize(72, 72));
|
||||
}
|
||||
}
|
||||
|
||||
tree->addTopLevelItem(item);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
tree->resizeColumnToContents(i);
|
||||
|
||||
if (textures.empty())
|
||||
{
|
||||
if (saved_only_count > 0 || scanned_only_count > 0)
|
||||
{
|
||||
info->setText(QObject::tr(
|
||||
"%1\nSaved filters and previously scanned textures are shown in gray.")
|
||||
.arg(config.empty_info_text));
|
||||
}
|
||||
else
|
||||
{
|
||||
info->setText(config.empty_info_text);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
info->setText(QObject::tr(
|
||||
"%1 current texture(s) for %4. %2 saved-only and %3 scanned-only shown in gray.\n"
|
||||
"Check rows and press Apply to update Texture Filters.")
|
||||
.arg(textures.size())
|
||||
.arg(saved_only_count)
|
||||
.arg(scanned_only_count)
|
||||
.arg(current_label));
|
||||
}
|
||||
};
|
||||
|
||||
QObject::connect(tree, &QTreeWidget::itemChanged, dlg,
|
||||
[selected_hashes, notify_live_selection](QTreeWidgetItem* item, int column) {
|
||||
if (!item || column != 0)
|
||||
return;
|
||||
|
||||
const u64 texture_hash = item->data(0, Qt::UserRole).toULongLong();
|
||||
const bool enabled = item->checkState(0) == Qt::Checked;
|
||||
if (enabled)
|
||||
selected_hashes->insert(texture_hash);
|
||||
else
|
||||
selected_hashes->erase(texture_hash);
|
||||
|
||||
notify_live_selection();
|
||||
});
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
|
||||
auto* refresh_button = buttons->addButton(QObject::tr("Refresh"), QDialogButtonBox::ActionRole);
|
||||
auto* apply_button = buttons->addButton(QObject::tr("Apply"), QDialogButtonBox::AcceptRole);
|
||||
|
||||
QObject::connect(buttons, &QDialogButtonBox::rejected, dlg, &QDialog::close);
|
||||
QObject::connect(refresh_button, &QPushButton::clicked, dlg, populate);
|
||||
QObject::connect(scan_timer, &QTimer::timeout, dlg, populate);
|
||||
QObject::connect(continuous_scan_check, &QCheckBox::toggled, dlg, [scan_timer, populate](bool checked) {
|
||||
if (checked)
|
||||
{
|
||||
populate();
|
||||
scan_timer->start();
|
||||
}
|
||||
else
|
||||
{
|
||||
scan_timer->stop();
|
||||
}
|
||||
});
|
||||
QObject::connect(apply_button, &QPushButton::clicked, dlg, [selected_hashes, config]() {
|
||||
if (!config.apply_selected_hashes)
|
||||
return;
|
||||
|
||||
std::vector<u64> hashes(selected_hashes->begin(), selected_hashes->end());
|
||||
std::sort(hashes.begin(), hashes.end());
|
||||
config.apply_selected_hashes(hashes);
|
||||
});
|
||||
|
||||
auto* layout = new QVBoxLayout;
|
||||
layout->addWidget(info);
|
||||
layout->addWidget(tree);
|
||||
auto* bottom_layout = new QHBoxLayout;
|
||||
bottom_layout->addWidget(continuous_scan_check);
|
||||
bottom_layout->addStretch();
|
||||
bottom_layout->addWidget(buttons);
|
||||
layout->addLayout(bottom_layout);
|
||||
dlg->setLayout(layout);
|
||||
|
||||
populate();
|
||||
QTimer::singleShot(200, dlg, populate);
|
||||
dlg->show();
|
||||
return dlg;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
class QWidget;
|
||||
|
||||
struct TextureHashBrowserEntry
|
||||
{
|
||||
u64 hash = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct TextureHashBrowserConfig
|
||||
{
|
||||
QString title;
|
||||
QString empty_info_text;
|
||||
QString current_label;
|
||||
std::function<QString()> fetch_current_label;
|
||||
std::vector<u64> initial_selected_hashes;
|
||||
std::function<std::vector<TextureHashBrowserEntry>()> fetch_current_entries;
|
||||
std::function<void(const std::vector<u64>&)> apply_selected_hashes;
|
||||
std::function<void(const std::vector<u64>&)> live_selection_changed;
|
||||
};
|
||||
|
||||
QDialog* ShowTextureHashBrowserDialog(QWidget* parent, const TextureHashBrowserConfig& config);
|
||||
@@ -0,0 +1,620 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Config/VRConfigWidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QStyle>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/IniFile.h"
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
#include "Core/Config/GraphicsSettings.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "DolphinQt/Config/GameConfigEdit.h"
|
||||
#include "DolphinQt/QtUtils/QtUtils.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view VR_SECTION_NAME = "Graphics.VR";
|
||||
constexpr std::string_view LEGACY_VR_SECTION_NAME = "GFX.VR";
|
||||
constexpr double VR_GAME_UPM_MIN = 0.01;
|
||||
constexpr double VR_GAME_UPM_MAX = 1000.0;
|
||||
constexpr double VR_GAME_UPM_STEP = 0.01;
|
||||
|
||||
void ResetTabPages(QTabWidget* tab)
|
||||
{
|
||||
while (tab->count() > 0)
|
||||
{
|
||||
QWidget* const page = tab->widget(0);
|
||||
tab->removeTab(0);
|
||||
delete page;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
VRConfigWidget::VRConfigWidget(std::string game_id, QWidget* parent)
|
||||
: QWidget(parent), m_game_id(std::move(game_id))
|
||||
{
|
||||
CreateWidgets();
|
||||
LoadFromFile();
|
||||
}
|
||||
|
||||
void VRConfigWidget::CreateWidgets()
|
||||
{
|
||||
auto* general_layout = new QVBoxLayout;
|
||||
|
||||
auto* description = new QLabel(
|
||||
tr("Configure per-game VR overrides saved to GameSettingsVR/<GameID>.ini."));
|
||||
description->setWordWrap(true);
|
||||
general_layout->addWidget(description);
|
||||
|
||||
auto* override_group = new QGroupBox(tr("Overrides"));
|
||||
auto* form = new QFormLayout;
|
||||
override_group->setLayout(form);
|
||||
|
||||
auto* units_row = new QWidget;
|
||||
auto* units_layout = new QHBoxLayout;
|
||||
units_layout->setContentsMargins(0, 0, 0, 0);
|
||||
units_row->setLayout(units_layout);
|
||||
|
||||
m_override_units_per_meter = new QCheckBox(tr("Override"));
|
||||
m_units_per_meter = new QDoubleSpinBox;
|
||||
m_units_per_meter->setRange(VR_GAME_UPM_MIN, VR_GAME_UPM_MAX);
|
||||
m_units_per_meter->setSingleStep(VR_GAME_UPM_STEP);
|
||||
m_units_per_meter->setDecimals(2);
|
||||
m_units_per_meter->setSuffix(tr(" units/m"));
|
||||
units_layout->addWidget(m_override_units_per_meter);
|
||||
units_layout->addWidget(m_units_per_meter, 1);
|
||||
|
||||
auto* lean_back_row = new QWidget;
|
||||
auto* lean_back_layout = new QHBoxLayout;
|
||||
lean_back_layout->setContentsMargins(0, 0, 0, 0);
|
||||
lean_back_row->setLayout(lean_back_layout);
|
||||
|
||||
m_override_lean_back_angle = new QCheckBox(tr("Override"));
|
||||
m_lean_back_angle = new QDoubleSpinBox;
|
||||
m_lean_back_angle->setRange(Config::GFX_VR_LEAN_BACK_ANGLE_MIN, Config::GFX_VR_LEAN_BACK_ANGLE_MAX);
|
||||
m_lean_back_angle->setSingleStep(Config::GFX_VR_LEAN_BACK_ANGLE_STEP);
|
||||
m_lean_back_angle->setDecimals(1);
|
||||
m_lean_back_angle->setSuffix(tr(" deg"));
|
||||
lean_back_layout->addWidget(m_override_lean_back_angle);
|
||||
lean_back_layout->addWidget(m_lean_back_angle, 1);
|
||||
|
||||
auto* camera_forward_row = new QWidget;
|
||||
auto* camera_forward_layout = new QHBoxLayout;
|
||||
camera_forward_layout->setContentsMargins(0, 0, 0, 0);
|
||||
camera_forward_row->setLayout(camera_forward_layout);
|
||||
|
||||
m_override_camera_forward = new QCheckBox(tr("Override"));
|
||||
m_camera_forward = new QDoubleSpinBox;
|
||||
m_camera_forward->setRange(Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX);
|
||||
m_camera_forward->setSingleStep(Config::GFX_VR_CAMERA_FORWARD_STEP);
|
||||
m_camera_forward->setDecimals(1);
|
||||
m_camera_forward->setSuffix(tr(" m"));
|
||||
camera_forward_layout->addWidget(m_override_camera_forward);
|
||||
camera_forward_layout->addWidget(m_camera_forward, 1);
|
||||
|
||||
auto* head_locked_curvature_row = new QWidget;
|
||||
auto* head_locked_curvature_layout = new QHBoxLayout;
|
||||
head_locked_curvature_layout->setContentsMargins(0, 0, 0, 0);
|
||||
head_locked_curvature_row->setLayout(head_locked_curvature_layout);
|
||||
|
||||
m_override_head_locked_curvature = new QCheckBox(tr("Override"));
|
||||
m_head_locked_curvature = new QDoubleSpinBox;
|
||||
m_head_locked_curvature->setRange(Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN,
|
||||
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX);
|
||||
m_head_locked_curvature->setSingleStep(Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP);
|
||||
m_head_locked_curvature->setDecimals(2);
|
||||
head_locked_curvature_layout->addWidget(m_override_head_locked_curvature);
|
||||
head_locked_curvature_layout->addWidget(m_head_locked_curvature, 1);
|
||||
|
||||
auto* element_depth_row = new QWidget;
|
||||
auto* element_depth_layout = new QHBoxLayout;
|
||||
element_depth_layout->setContentsMargins(0, 0, 0, 0);
|
||||
element_depth_row->setLayout(element_depth_layout);
|
||||
|
||||
m_override_element_depth = new QCheckBox(tr("Override"));
|
||||
m_element_depth = new QDoubleSpinBox;
|
||||
m_element_depth->setRange(Config::GFX_VR_ELEMENT_DEPTH_MIN, Config::GFX_VR_ELEMENT_DEPTH_MAX);
|
||||
m_element_depth->setSingleStep(Config::GFX_VR_ELEMENT_DEPTH_STEP);
|
||||
m_element_depth->setDecimals(4);
|
||||
element_depth_layout->addWidget(m_override_element_depth);
|
||||
element_depth_layout->addWidget(m_element_depth, 1);
|
||||
|
||||
m_virtual_screen_mode = new QComboBox;
|
||||
m_dont_clear_screen_mode = new QComboBox;
|
||||
m_opcode_replay_mode = new QComboBox;
|
||||
m_force_vbi_90hz_mode = new QComboBox;
|
||||
PopulateBoolModeCombo(m_virtual_screen_mode);
|
||||
PopulateBoolModeCombo(m_dont_clear_screen_mode);
|
||||
PopulateReplayModeCombo(m_opcode_replay_mode);
|
||||
PopulateBoolModeCombo(m_force_vbi_90hz_mode);
|
||||
|
||||
form->addRow(tr("Units per Meter"), units_row);
|
||||
form->addRow(tr("Lean Back Angle"), lean_back_row);
|
||||
form->addRow(tr("Camera Forward"), camera_forward_row);
|
||||
form->addRow(tr("Head Locked Curvature"), head_locked_curvature_row);
|
||||
form->addRow(tr("Element Depth"), element_depth_row);
|
||||
form->addRow(tr("Virtual Screen"), m_virtual_screen_mode);
|
||||
form->addRow(tr("Don't Clear Screen"), m_dont_clear_screen_mode);
|
||||
form->addRow(tr("Opcode Replay"), m_opcode_replay_mode);
|
||||
form->addRow(tr("Force VBI Frequency to 90 Hz in VR"), m_force_vbi_90hz_mode);
|
||||
|
||||
general_layout->addWidget(override_group);
|
||||
general_layout->addStretch();
|
||||
|
||||
auto* const general_widget = new QWidget;
|
||||
general_widget->setLayout(general_layout);
|
||||
|
||||
auto* editor_layout = new QVBoxLayout;
|
||||
|
||||
auto* local_group = new QGroupBox(tr("VR User Config"));
|
||||
auto* local_layout = new QVBoxLayout;
|
||||
m_local_tab = new QTabWidget;
|
||||
local_layout->addWidget(m_local_tab);
|
||||
local_group->setLayout(local_layout);
|
||||
|
||||
editor_layout->addWidget(local_group);
|
||||
|
||||
auto* const editor_widget = new QWidget;
|
||||
editor_widget->setLayout(editor_layout);
|
||||
|
||||
auto* const tabs = new QTabWidget;
|
||||
tabs->addTab(general_widget, tr("General"));
|
||||
const int editor_index = tabs->addTab(editor_widget, tr("Editor"));
|
||||
|
||||
auto* const root_layout = new QVBoxLayout;
|
||||
auto* const help_label = new QLabel(tr("These VR settings override core Dolphin settings."));
|
||||
auto* const help_widget =
|
||||
QtUtils::CreateIconWarning(this, QStyle::SP_MessageBoxQuestion, help_label);
|
||||
root_layout->addWidget(help_widget);
|
||||
root_layout->addWidget(tabs);
|
||||
setLayout(root_layout);
|
||||
|
||||
connect(m_override_units_per_meter, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
m_units_per_meter->setEnabled(checked);
|
||||
SaveToFile();
|
||||
});
|
||||
connect(m_units_per_meter, &QDoubleSpinBox::valueChanged, this,
|
||||
[this](double) { SaveToFile(); });
|
||||
connect(m_override_lean_back_angle, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
m_lean_back_angle->setEnabled(checked);
|
||||
SaveToFile();
|
||||
});
|
||||
connect(m_lean_back_angle, &QDoubleSpinBox::valueChanged, this,
|
||||
[this](double) { SaveToFile(); });
|
||||
connect(m_override_camera_forward, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
m_camera_forward->setEnabled(checked);
|
||||
SaveToFile();
|
||||
});
|
||||
connect(m_camera_forward, &QDoubleSpinBox::valueChanged, this,
|
||||
[this](double) { SaveToFile(); });
|
||||
connect(m_override_head_locked_curvature, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
m_head_locked_curvature->setEnabled(checked);
|
||||
SaveToFile();
|
||||
});
|
||||
connect(m_head_locked_curvature, &QDoubleSpinBox::valueChanged, this,
|
||||
[this](double) { SaveToFile(); });
|
||||
connect(m_override_element_depth, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
m_element_depth->setEnabled(checked);
|
||||
SaveToFile();
|
||||
});
|
||||
connect(m_element_depth, &QDoubleSpinBox::valueChanged, this,
|
||||
[this](double) { SaveToFile(); });
|
||||
connect(m_virtual_screen_mode, &QComboBox::currentIndexChanged, this,
|
||||
[this](int) { SaveToFile(); });
|
||||
connect(m_dont_clear_screen_mode, &QComboBox::currentIndexChanged, this,
|
||||
[this](int) { SaveToFile(); });
|
||||
connect(m_opcode_replay_mode, &QComboBox::currentIndexChanged, this,
|
||||
[this](int) { SaveToFile(); });
|
||||
connect(m_force_vbi_90hz_mode, &QComboBox::currentIndexChanged, this,
|
||||
[this](int) { SaveToFile(); });
|
||||
|
||||
connect(tabs, &QTabWidget::currentChanged, this, [this, editor_index](int index) {
|
||||
// Sync editor content with current file after changes from General.
|
||||
if (index == editor_index)
|
||||
RefreshEditorTabs();
|
||||
|
||||
// Reload controls after raw INI edits.
|
||||
if (m_prev_tab_index == editor_index)
|
||||
{
|
||||
LoadFromFile();
|
||||
if (SConfig::GetInstance().GetGameID() == m_game_id)
|
||||
SConfig::GetInstance().ReloadGameVRConfigOverrides();
|
||||
}
|
||||
|
||||
m_prev_tab_index = index;
|
||||
});
|
||||
|
||||
RefreshEditorTabs();
|
||||
}
|
||||
|
||||
void VRConfigWidget::LoadFromFile()
|
||||
{
|
||||
const ValueMap values = ReadVRSectionValues(GetINIPath());
|
||||
|
||||
m_updating = true;
|
||||
|
||||
const auto units_it = values.find("UnitsPerMeter");
|
||||
const bool units_overridden = units_it != values.end();
|
||||
m_override_units_per_meter->setChecked(units_overridden);
|
||||
m_units_per_meter->setEnabled(units_overridden);
|
||||
|
||||
double units_per_meter = Config::GetBase(Config::GFX_VR_UNITS_PER_METER);
|
||||
if (units_overridden)
|
||||
{
|
||||
double parsed = units_per_meter;
|
||||
if (TryParse(units_it->second, &parsed))
|
||||
units_per_meter = parsed;
|
||||
}
|
||||
units_per_meter =
|
||||
std::clamp(units_per_meter, VR_GAME_UPM_MIN, VR_GAME_UPM_MAX);
|
||||
m_units_per_meter->setValue(units_per_meter);
|
||||
|
||||
const auto lean_back_it = values.find("LeanBackAngle");
|
||||
const bool lean_back_overridden = lean_back_it != values.end();
|
||||
m_override_lean_back_angle->setChecked(lean_back_overridden);
|
||||
m_lean_back_angle->setEnabled(lean_back_overridden);
|
||||
|
||||
double lean_back_angle = Config::GetBase(Config::GFX_VR_LEAN_BACK_ANGLE);
|
||||
if (lean_back_overridden)
|
||||
{
|
||||
double parsed = lean_back_angle;
|
||||
if (TryParse(lean_back_it->second, &parsed))
|
||||
lean_back_angle = parsed;
|
||||
}
|
||||
lean_back_angle =
|
||||
std::clamp(lean_back_angle, static_cast<double>(Config::GFX_VR_LEAN_BACK_ANGLE_MIN),
|
||||
static_cast<double>(Config::GFX_VR_LEAN_BACK_ANGLE_MAX));
|
||||
m_lean_back_angle->setValue(lean_back_angle);
|
||||
|
||||
const auto camera_forward_it = values.find("CameraForward");
|
||||
const bool camera_forward_overridden = camera_forward_it != values.end();
|
||||
m_override_camera_forward->setChecked(camera_forward_overridden);
|
||||
m_camera_forward->setEnabled(camera_forward_overridden);
|
||||
|
||||
double camera_forward = Config::GetBase(Config::GFX_VR_CAMERA_FORWARD);
|
||||
if (camera_forward_overridden)
|
||||
{
|
||||
double parsed = camera_forward;
|
||||
if (TryParse(camera_forward_it->second, &parsed))
|
||||
camera_forward = parsed;
|
||||
}
|
||||
camera_forward =
|
||||
std::clamp(camera_forward, static_cast<double>(Config::GFX_VR_CAMERA_FORWARD_MIN),
|
||||
static_cast<double>(Config::GFX_VR_CAMERA_FORWARD_MAX));
|
||||
m_camera_forward->setValue(camera_forward);
|
||||
|
||||
const auto head_locked_curvature_it = values.find("HeadLockedCurvature");
|
||||
const bool head_locked_curvature_overridden = head_locked_curvature_it != values.end();
|
||||
m_override_head_locked_curvature->setChecked(head_locked_curvature_overridden);
|
||||
m_head_locked_curvature->setEnabled(head_locked_curvature_overridden);
|
||||
|
||||
double head_locked_curvature = Config::GetBase(Config::GFX_VR_HEAD_LOCKED_CURVATURE);
|
||||
if (head_locked_curvature_overridden)
|
||||
{
|
||||
double parsed = head_locked_curvature;
|
||||
if (TryParse(head_locked_curvature_it->second, &parsed))
|
||||
head_locked_curvature = parsed;
|
||||
}
|
||||
head_locked_curvature =
|
||||
std::clamp(head_locked_curvature, static_cast<double>(Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN),
|
||||
static_cast<double>(Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX));
|
||||
m_head_locked_curvature->setValue(head_locked_curvature);
|
||||
|
||||
const auto element_depth_it = values.find("ElementDepth");
|
||||
const bool element_depth_overridden = element_depth_it != values.end();
|
||||
m_override_element_depth->setChecked(element_depth_overridden);
|
||||
m_element_depth->setEnabled(element_depth_overridden);
|
||||
|
||||
double element_depth = Config::GetBase(Config::GFX_VR_ELEMENT_DEPTH);
|
||||
if (element_depth_overridden)
|
||||
{
|
||||
double parsed = element_depth;
|
||||
if (TryParse(element_depth_it->second, &parsed))
|
||||
element_depth = parsed;
|
||||
}
|
||||
element_depth =
|
||||
std::clamp(element_depth, static_cast<double>(Config::GFX_VR_ELEMENT_DEPTH_MIN),
|
||||
static_cast<double>(Config::GFX_VR_ELEMENT_DEPTH_MAX));
|
||||
m_element_depth->setValue(element_depth);
|
||||
|
||||
SetBoolMode(m_virtual_screen_mode, ParseBoolMode(values, "VirtualScreen"));
|
||||
SetBoolMode(m_dont_clear_screen_mode, ParseBoolMode(values, "DontClearScreen"));
|
||||
SetReplayMode(m_opcode_replay_mode, ParseReplayMode(values, "OpcodeReplay"));
|
||||
SetBoolMode(m_force_vbi_90hz_mode, ParseBoolMode(values, "AutoVBIFromHMD"));
|
||||
|
||||
m_updating = false;
|
||||
}
|
||||
|
||||
void VRConfigWidget::SaveToFile()
|
||||
{
|
||||
if (m_updating || m_game_id.empty())
|
||||
return;
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> entries;
|
||||
if (m_override_units_per_meter->isChecked())
|
||||
{
|
||||
entries.emplace_back("UnitsPerMeter",
|
||||
QString::number(m_units_per_meter->value(), 'f', 2).toStdString());
|
||||
}
|
||||
if (m_override_lean_back_angle->isChecked())
|
||||
{
|
||||
entries.emplace_back("LeanBackAngle",
|
||||
QString::number(m_lean_back_angle->value(), 'f', 1).toStdString());
|
||||
}
|
||||
if (m_override_camera_forward->isChecked())
|
||||
{
|
||||
entries.emplace_back("CameraForward",
|
||||
QString::number(m_camera_forward->value(), 'f', 1).toStdString());
|
||||
}
|
||||
if (m_override_head_locked_curvature->isChecked())
|
||||
{
|
||||
entries.emplace_back("HeadLockedCurvature",
|
||||
QString::number(m_head_locked_curvature->value(), 'f', 2).toStdString());
|
||||
}
|
||||
if (m_override_element_depth->isChecked())
|
||||
{
|
||||
entries.emplace_back("ElementDepth",
|
||||
QString::number(m_element_depth->value(), 'f', 4).toStdString());
|
||||
}
|
||||
|
||||
const auto append_bool = [&entries](const char* key, BoolMode mode) {
|
||||
if (mode == BoolMode::Inherit)
|
||||
return;
|
||||
entries.emplace_back(key, mode == BoolMode::Enabled ? "True" : "False");
|
||||
};
|
||||
append_bool("VirtualScreen", GetBoolMode(m_virtual_screen_mode));
|
||||
append_bool("DontClearScreen", GetBoolMode(m_dont_clear_screen_mode));
|
||||
switch (GetReplayMode(m_opcode_replay_mode))
|
||||
{
|
||||
case ReplayMode::Off:
|
||||
entries.emplace_back("OpcodeReplay", "0");
|
||||
break;
|
||||
case ReplayMode::Replay60To90:
|
||||
entries.emplace_back("OpcodeReplay", "1");
|
||||
break;
|
||||
case ReplayMode::Replay30To90:
|
||||
entries.emplace_back("OpcodeReplay", "2");
|
||||
break;
|
||||
case ReplayMode::Inherit:
|
||||
break;
|
||||
}
|
||||
append_bool("AutoVBIFromHMD", GetBoolMode(m_force_vbi_90hz_mode));
|
||||
|
||||
const std::string path = GetINIPath();
|
||||
std::string base = ReadFileWithoutVRSection(path);
|
||||
while (!base.empty() &&
|
||||
(base.back() == '\n' || base.back() == '\r' ||
|
||||
std::isspace(static_cast<unsigned char>(base.back()))))
|
||||
base.pop_back();
|
||||
|
||||
std::ostringstream out;
|
||||
if (!base.empty())
|
||||
out << base << "\n";
|
||||
|
||||
if (!entries.empty())
|
||||
{
|
||||
out << '[' << VR_SECTION_NAME << "]\n";
|
||||
for (const auto& [key, value] : entries)
|
||||
out << key << " = " << value << "\n";
|
||||
}
|
||||
|
||||
const std::string output = out.str();
|
||||
if (output.empty())
|
||||
{
|
||||
File::Delete(path, File::IfAbsentBehavior::NoConsoleWarning);
|
||||
}
|
||||
else
|
||||
{
|
||||
File::CreateFullPath(path);
|
||||
std::ofstream file(path, std::ios::trunc);
|
||||
file << output;
|
||||
}
|
||||
|
||||
if (SConfig::GetInstance().GetGameID() == m_game_id)
|
||||
SConfig::GetInstance().ReloadGameVRConfigOverrides();
|
||||
}
|
||||
|
||||
void VRConfigWidget::RefreshEditorTabs()
|
||||
{
|
||||
if (!m_local_tab || m_game_id.empty())
|
||||
return;
|
||||
|
||||
ResetTabPages(m_local_tab);
|
||||
|
||||
const std::string local_path = GetINIPath();
|
||||
m_local_tab->addTab(new GameConfigEdit(nullptr, QString::fromStdString(local_path), false),
|
||||
QString::fromStdString(m_game_id + ".ini"));
|
||||
}
|
||||
|
||||
std::string VRConfigWidget::GetINIPath() const
|
||||
{
|
||||
return File::GetUserPath(D_GAMESETTINGSVR_IDX) + m_game_id + ".ini";
|
||||
}
|
||||
|
||||
bool VRConfigWidget::IsVRSectionHeader(std::string_view line)
|
||||
{
|
||||
const std::string_view trimmed = StripWhitespace(line);
|
||||
if (trimmed.size() < 3 || trimmed.front() != '[' || trimmed.back() != ']')
|
||||
return false;
|
||||
|
||||
const std::string_view section_name = trimmed.substr(1, trimmed.size() - 2);
|
||||
return Common::CaseInsensitiveEquals(section_name, VR_SECTION_NAME) ||
|
||||
Common::CaseInsensitiveEquals(section_name, LEGACY_VR_SECTION_NAME);
|
||||
}
|
||||
|
||||
std::string VRConfigWidget::ReadFileWithoutVRSection(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))
|
||||
{
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line.pop_back();
|
||||
|
||||
const std::string_view trimmed = StripWhitespace(line);
|
||||
if (IsVRSectionHeader(trimmed))
|
||||
{
|
||||
skipping = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (skipping && !trimmed.empty() && trimmed.front() == '[')
|
||||
skipping = false;
|
||||
|
||||
if (!skipping)
|
||||
out << line << "\n";
|
||||
}
|
||||
|
||||
return out.str();
|
||||
}
|
||||
|
||||
VRConfigWidget::ValueMap VRConfigWidget::ReadVRSectionValues(const std::string& path)
|
||||
{
|
||||
ValueMap values;
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open())
|
||||
return values;
|
||||
|
||||
bool in_section = false;
|
||||
std::string line;
|
||||
while (std::getline(file, line))
|
||||
{
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line.pop_back();
|
||||
|
||||
const std::string_view trimmed = StripWhitespace(line);
|
||||
if (trimmed.empty())
|
||||
continue;
|
||||
|
||||
if (IsVRSectionHeader(trimmed))
|
||||
{
|
||||
in_section = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_section && trimmed.front() == '[')
|
||||
break;
|
||||
|
||||
if (!in_section)
|
||||
continue;
|
||||
|
||||
std::string key;
|
||||
std::string value;
|
||||
Common::IniFile::ParseLine(trimmed, &key, &value);
|
||||
if (!key.empty())
|
||||
values.insert_or_assign(std::move(key), std::move(value));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
VRConfigWidget::BoolMode VRConfigWidget::ParseBoolMode(const ValueMap& values, const char* key)
|
||||
{
|
||||
const auto it = values.find(key);
|
||||
if (it == values.end())
|
||||
return BoolMode::Inherit;
|
||||
|
||||
bool parsed = false;
|
||||
if (TryParse(it->second, &parsed))
|
||||
return parsed ? BoolMode::Enabled : BoolMode::Disabled;
|
||||
return BoolMode::Inherit;
|
||||
}
|
||||
|
||||
VRConfigWidget::ReplayMode VRConfigWidget::ParseReplayMode(const ValueMap& values, const char* key)
|
||||
{
|
||||
const auto it = values.find(key);
|
||||
if (it == values.end())
|
||||
return ReplayMode::Inherit;
|
||||
|
||||
int parsed = 0;
|
||||
if (!TryParse(it->second, &parsed))
|
||||
return ReplayMode::Inherit;
|
||||
|
||||
switch (parsed)
|
||||
{
|
||||
case 0:
|
||||
return ReplayMode::Off;
|
||||
case 1:
|
||||
return ReplayMode::Replay60To90;
|
||||
case 2:
|
||||
return ReplayMode::Replay30To90;
|
||||
default:
|
||||
return ReplayMode::Inherit;
|
||||
}
|
||||
}
|
||||
|
||||
void VRConfigWidget::SetBoolMode(QComboBox* combo, BoolMode mode)
|
||||
{
|
||||
combo->setCurrentIndex(static_cast<int>(mode));
|
||||
}
|
||||
|
||||
VRConfigWidget::BoolMode VRConfigWidget::GetBoolMode(const QComboBox* combo)
|
||||
{
|
||||
const int index = combo->currentIndex();
|
||||
if (index == static_cast<int>(BoolMode::Enabled))
|
||||
return BoolMode::Enabled;
|
||||
if (index == static_cast<int>(BoolMode::Disabled))
|
||||
return BoolMode::Disabled;
|
||||
return BoolMode::Inherit;
|
||||
}
|
||||
|
||||
void VRConfigWidget::PopulateBoolModeCombo(QComboBox* combo)
|
||||
{
|
||||
combo->addItem(QObject::tr("Inherit"));
|
||||
combo->addItem(QObject::tr("Enabled"));
|
||||
combo->addItem(QObject::tr("Disabled"));
|
||||
}
|
||||
|
||||
void VRConfigWidget::SetReplayMode(QComboBox* combo, ReplayMode mode)
|
||||
{
|
||||
combo->setCurrentIndex(static_cast<int>(mode) + 1);
|
||||
}
|
||||
|
||||
VRConfigWidget::ReplayMode VRConfigWidget::GetReplayMode(const QComboBox* combo)
|
||||
{
|
||||
switch (combo->currentIndex() - 1)
|
||||
{
|
||||
case 0:
|
||||
return ReplayMode::Off;
|
||||
case 1:
|
||||
return ReplayMode::Replay60To90;
|
||||
case 2:
|
||||
return ReplayMode::Replay30To90;
|
||||
default:
|
||||
return ReplayMode::Inherit;
|
||||
}
|
||||
}
|
||||
|
||||
void VRConfigWidget::PopulateReplayModeCombo(QComboBox* combo)
|
||||
{
|
||||
combo->addItem(QObject::tr("Inherit"));
|
||||
combo->addItem(QObject::tr("Off"));
|
||||
combo->addItem(QObject::tr("60 to 90"));
|
||||
combo->addItem(QObject::tr("30 to 90"));
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "Common/StringUtil.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QTabWidget;
|
||||
|
||||
class VRConfigWidget final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit VRConfigWidget(std::string game_id, QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
enum class BoolMode
|
||||
{
|
||||
Inherit = 0,
|
||||
Enabled = 1,
|
||||
Disabled = 2,
|
||||
};
|
||||
|
||||
enum class ReplayMode
|
||||
{
|
||||
Inherit = -1,
|
||||
Off = 0,
|
||||
Replay60To90 = 1,
|
||||
Replay30To90 = 2,
|
||||
};
|
||||
|
||||
using ValueMap = std::map<std::string, std::string, Common::CaseInsensitiveLess>;
|
||||
|
||||
void CreateWidgets();
|
||||
void LoadFromFile();
|
||||
void SaveToFile();
|
||||
void RefreshEditorTabs();
|
||||
|
||||
std::string GetINIPath() const;
|
||||
static bool IsVRSectionHeader(std::string_view line);
|
||||
static std::string ReadFileWithoutVRSection(const std::string& path);
|
||||
static ValueMap ReadVRSectionValues(const std::string& path);
|
||||
static BoolMode ParseBoolMode(const ValueMap& values, const char* key);
|
||||
static ReplayMode ParseReplayMode(const ValueMap& values, const char* key);
|
||||
static void SetBoolMode(QComboBox* combo, BoolMode mode);
|
||||
static BoolMode GetBoolMode(const QComboBox* combo);
|
||||
static void PopulateBoolModeCombo(QComboBox* combo);
|
||||
static void SetReplayMode(QComboBox* combo, ReplayMode mode);
|
||||
static ReplayMode GetReplayMode(const QComboBox* combo);
|
||||
static void PopulateReplayModeCombo(QComboBox* combo);
|
||||
|
||||
const std::string m_game_id;
|
||||
bool m_updating = false;
|
||||
|
||||
QCheckBox* m_override_units_per_meter = nullptr;
|
||||
QDoubleSpinBox* m_units_per_meter = nullptr;
|
||||
QCheckBox* m_override_lean_back_angle = nullptr;
|
||||
QDoubleSpinBox* m_lean_back_angle = nullptr;
|
||||
QCheckBox* m_override_camera_forward = nullptr;
|
||||
QDoubleSpinBox* m_camera_forward = nullptr;
|
||||
QCheckBox* m_override_head_locked_curvature = nullptr;
|
||||
QDoubleSpinBox* m_head_locked_curvature = nullptr;
|
||||
QCheckBox* m_override_element_depth = nullptr;
|
||||
QDoubleSpinBox* m_element_depth = nullptr;
|
||||
QComboBox* m_virtual_screen_mode = nullptr;
|
||||
QComboBox* m_dont_clear_screen_mode = nullptr;
|
||||
QComboBox* m_opcode_replay_mode = nullptr;
|
||||
QComboBox* m_force_vbi_90hz_mode = nullptr;
|
||||
|
||||
QTabWidget* m_local_tab = nullptr;
|
||||
int m_prev_tab_index = 0;
|
||||
};
|
||||
@@ -46,6 +46,23 @@
|
||||
#include "Core/HW//WiimoteReal/IOWin.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
WiimoteSource GetSourceFromCombo(const QComboBox* combo)
|
||||
{
|
||||
const QVariant value = combo->currentData();
|
||||
if (!value.isValid())
|
||||
return WiimoteSource::None;
|
||||
|
||||
return static_cast<WiimoteSource>(value.toInt());
|
||||
}
|
||||
|
||||
bool IsConfigurableWiimoteSource(WiimoteSource source)
|
||||
{
|
||||
return source == WiimoteSource::Emulated || source == WiimoteSource::OpenXR;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
WiimoteControllersWidget::WiimoteControllersWidget(QWidget* parent) : QWidget(parent)
|
||||
{
|
||||
CreateLayout();
|
||||
@@ -261,8 +278,10 @@ void WiimoteControllersWidget::CreateLayout()
|
||||
auto* wm_box = m_wiimote_boxes[i] = new QComboBox();
|
||||
auto* wm_button = m_wiimote_buttons[i] = new NonDefaultQPushButton(tr("Configure"));
|
||||
|
||||
for (const auto& item : {tr("None"), tr("Emulated Wii Remote"), tr("Real Wii Remote")})
|
||||
wm_box->addItem(item);
|
||||
wm_box->addItem(tr("None"), static_cast<int>(WiimoteSource::None));
|
||||
wm_box->addItem(tr("Emulated Wii Remote"), static_cast<int>(WiimoteSource::Emulated));
|
||||
wm_box->addItem(tr("OpenXR Wii Remote"), static_cast<int>(WiimoteSource::OpenXR));
|
||||
wm_box->addItem(tr("Real Wii Remote"), static_cast<int>(WiimoteSource::Real));
|
||||
|
||||
int wm_row = m_wiimote_layout->rowCount();
|
||||
m_wiimote_layout->addWidget(wm_label, wm_row, 1);
|
||||
@@ -417,12 +436,13 @@ void WiimoteControllersWidget::OnWiimoteRefreshPressed()
|
||||
void WiimoteControllersWidget::OnWiimoteConfigure(size_t index)
|
||||
{
|
||||
MappingWindow::Type type;
|
||||
switch (m_wiimote_boxes[index]->currentIndex())
|
||||
switch (GetSourceFromCombo(m_wiimote_boxes[index]))
|
||||
{
|
||||
case 0: // None
|
||||
case 2: // Real Wii Remote
|
||||
case WiimoteSource::None:
|
||||
case WiimoteSource::Real:
|
||||
return;
|
||||
case 1: // Emulated Wii Remote
|
||||
case WiimoteSource::Emulated:
|
||||
case WiimoteSource::OpenXR:
|
||||
type = MappingWindow::Type::MAPPING_WIIMOTE_EMU;
|
||||
break;
|
||||
default:
|
||||
@@ -452,8 +472,14 @@ void WiimoteControllersWidget::LoadSettings(Core::State state)
|
||||
{
|
||||
for (size_t i = 0; i < m_wiimote_groups.size(); i++)
|
||||
{
|
||||
SignalBlocking(m_wiimote_boxes[i])
|
||||
->setCurrentIndex(int(Config::Get(Config::GetInfoForWiimoteSource(int(i)))));
|
||||
const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(int(i)));
|
||||
int source_index = m_wiimote_boxes[i]->findData(static_cast<int>(source));
|
||||
if (source_index < 0)
|
||||
source_index = m_wiimote_boxes[i]->findData(static_cast<int>(WiimoteSource::None));
|
||||
if (source_index < 0)
|
||||
source_index = 0;
|
||||
|
||||
SignalBlocking(m_wiimote_boxes[i])->setCurrentIndex(source_index);
|
||||
}
|
||||
SignalBlocking(m_wiimote_real_balance_board)
|
||||
->setChecked(Config::Get(Config::WIIMOTE_BB_SOURCE) == WiimoteSource::Real);
|
||||
@@ -497,8 +523,8 @@ void WiimoteControllersWidget::LoadSettings(Core::State state)
|
||||
m_wiimote_labels[i]->setEnabled(enable_emu_bt);
|
||||
m_wiimote_boxes[i]->setEnabled(enable_emu_bt && !running_netplay);
|
||||
|
||||
const bool is_emu_wiimote = m_wiimote_boxes[i]->currentIndex() == 1;
|
||||
m_wiimote_buttons[i]->setEnabled(enable_emu_bt && is_emu_wiimote &&
|
||||
const WiimoteSource source = GetSourceFromCombo(m_wiimote_boxes[i]);
|
||||
m_wiimote_buttons[i]->setEnabled(enable_emu_bt && IsConfigurableWiimoteSource(source) &&
|
||||
static_cast<int>(i) < num_local_wiimotes);
|
||||
}
|
||||
|
||||
@@ -531,8 +557,8 @@ void WiimoteControllersWidget::SaveSettings()
|
||||
|
||||
for (size_t i = 0; i < m_wiimote_groups.size(); i++)
|
||||
{
|
||||
const int index = m_wiimote_boxes[i]->currentIndex();
|
||||
Config::SetBaseOrCurrent(Config::GetInfoForWiimoteSource(int(i)), WiimoteSource(index));
|
||||
const WiimoteSource source = GetSourceFromCombo(m_wiimote_boxes[i]);
|
||||
Config::SetBaseOrCurrent(Config::GetInfoForWiimoteSource(int(i)), source);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,767 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Debugger/CullingCodeFinderWidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QCloseEvent>
|
||||
#include <QDesktopServices>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QGroupBox>
|
||||
#include <QGuiApplication>
|
||||
#include <QHeaderView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSpinBox>
|
||||
#include <QTableWidget>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/CommonPaths.h"
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/IniFile.h"
|
||||
#include "Core/ActionReplay.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/PowerPC/PPCSymbolDB.h"
|
||||
#include "Core/System.h"
|
||||
#include "DolphinQt/Host.h"
|
||||
#include "VideoCommon/CullingCodeFinder.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
u32 ParseOptionalHexField(const QString& text, bool* ok)
|
||||
{
|
||||
QString parsed_text = text.trimmed();
|
||||
if (parsed_text.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
||||
parsed_text.remove(0, 2);
|
||||
|
||||
*ok = parsed_text.isEmpty();
|
||||
return parsed_text.isEmpty() ? 0u : parsed_text.toUInt(ok, 16);
|
||||
}
|
||||
|
||||
QString PathLeaf(QString path)
|
||||
{
|
||||
return QFileInfo(path).fileName();
|
||||
}
|
||||
|
||||
QString PathLeaf(const std::string& path)
|
||||
{
|
||||
return QFileInfo(QString::fromStdString(path)).fileName();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CullingCodeFinderWidget::CullingCodeFinderWidget(QWidget* parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Culling Code Finder"));
|
||||
setMinimumWidth(840);
|
||||
|
||||
CreateWidgets();
|
||||
ConnectSignals();
|
||||
RefreshStaticInfo();
|
||||
UpdateButtonStates();
|
||||
|
||||
m_update_timer = new QTimer(this);
|
||||
connect(m_update_timer, &QTimer::timeout, this, &CullingCodeFinderWidget::UpdateDisplay);
|
||||
m_update_timer->start(16);
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
auto& finder = CullingCodeFinder::GetInstance();
|
||||
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning ||
|
||||
finder.GetState() == CullingCodeFinder::ScanState::Paused)
|
||||
{
|
||||
finder.StopScan();
|
||||
finder.AdvanceScan(Core::System::GetInstance());
|
||||
}
|
||||
|
||||
m_update_timer->stop();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::CreateWidgets()
|
||||
{
|
||||
auto* main_layout = new QVBoxLayout(this);
|
||||
|
||||
auto* stats_group = new QGroupBox(tr("Live Statistics"));
|
||||
auto* stats_layout = new QHBoxLayout;
|
||||
stats_group->setLayout(stats_layout);
|
||||
|
||||
stats_layout->addWidget(new QLabel(tr("Draw Calls:")));
|
||||
m_live_draw_calls = new QLabel(QStringLiteral("0"));
|
||||
stats_layout->addWidget(m_live_draw_calls);
|
||||
stats_layout->addWidget(new QLabel(tr("Triangles:")));
|
||||
m_live_triangles = new QLabel(QStringLiteral("0"));
|
||||
stats_layout->addWidget(m_live_triangles);
|
||||
stats_layout->addWidget(new QLabel(tr("Prims:")));
|
||||
m_live_prims = new QLabel(QStringLiteral("0"));
|
||||
stats_layout->addWidget(m_live_prims);
|
||||
stats_layout->addWidget(new QLabel(tr("Objects:")));
|
||||
m_live_objects = new QLabel(QStringLiteral("0"));
|
||||
stats_layout->addWidget(m_live_objects);
|
||||
stats_layout->addStretch();
|
||||
main_layout->addWidget(stats_group);
|
||||
|
||||
auto* baseline_group = new QGroupBox(tr("Baseline"));
|
||||
auto* baseline_layout = new QHBoxLayout;
|
||||
baseline_group->setLayout(baseline_layout);
|
||||
m_baseline_label = new QLabel(tr("Not set"));
|
||||
baseline_layout->addWidget(m_baseline_label);
|
||||
baseline_layout->addStretch();
|
||||
m_set_baseline_btn = new QPushButton(tr("Set Baseline"));
|
||||
baseline_layout->addWidget(m_set_baseline_btn);
|
||||
main_layout->addWidget(baseline_group);
|
||||
|
||||
auto* prerequisite_group = new QGroupBox(tr("Prerequisites"));
|
||||
auto* prerequisite_layout = new QVBoxLayout;
|
||||
prerequisite_group->setLayout(prerequisite_layout);
|
||||
|
||||
auto* map_layout = new QHBoxLayout;
|
||||
map_layout->addWidget(new QLabel(tr("Map File:")));
|
||||
m_map_path_label = new QLabel(tr("Not found"));
|
||||
m_map_path_label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
map_layout->addWidget(m_map_path_label, 1);
|
||||
m_generate_map_btn = new QPushButton(tr("Generate .map"));
|
||||
map_layout->addWidget(m_generate_map_btn);
|
||||
prerequisite_layout->addLayout(map_layout);
|
||||
|
||||
auto* output_layout = new QHBoxLayout;
|
||||
output_layout->addWidget(new QLabel(tr("Output Folder:")));
|
||||
m_output_folder_edit = new QLineEdit;
|
||||
output_layout->addWidget(m_output_folder_edit, 1);
|
||||
m_browse_output_btn = new QPushButton(tr("Browse"));
|
||||
m_open_output_btn = new QPushButton(tr("Open"));
|
||||
output_layout->addWidget(m_browse_output_btn);
|
||||
output_layout->addWidget(m_open_output_btn);
|
||||
prerequisite_layout->addLayout(output_layout);
|
||||
main_layout->addWidget(prerequisite_group);
|
||||
|
||||
auto* config_group = new QGroupBox(tr("Scan Configuration"));
|
||||
auto* config_layout = new QVBoxLayout;
|
||||
config_group->setLayout(config_layout);
|
||||
|
||||
auto* top_row = new QHBoxLayout;
|
||||
top_row->addWidget(new QLabel(tr("Savestate Slot:")));
|
||||
m_savestate_slot_spin = new QSpinBox;
|
||||
m_savestate_slot_spin->setRange(1, 10);
|
||||
m_savestate_slot_spin->setValue(1);
|
||||
top_row->addWidget(m_savestate_slot_spin);
|
||||
top_row->addWidget(new QLabel(tr("Settle Frames:")));
|
||||
m_settle_frames_spin = new QSpinBox;
|
||||
m_settle_frames_spin->setRange(1, 60);
|
||||
m_settle_frames_spin->setValue(5);
|
||||
top_row->addWidget(m_settle_frames_spin);
|
||||
top_row->addWidget(new QLabel(tr("Candidate Limit:")));
|
||||
m_candidate_limit_spin = new QSpinBox;
|
||||
m_candidate_limit_spin->setRange(0, 1000000);
|
||||
m_candidate_limit_spin->setValue(0);
|
||||
m_candidate_limit_spin->setSpecialValueText(tr("Unlimited"));
|
||||
top_row->addWidget(m_candidate_limit_spin);
|
||||
top_row->addStretch();
|
||||
config_layout->addLayout(top_row);
|
||||
|
||||
m_settle_frames_help = new QLabel(
|
||||
tr("Settle Frames are emulated frames after each savestate reload. Turbo only reduces wall-"
|
||||
"clock scan time."));
|
||||
m_settle_frames_help->setWordWrap(true);
|
||||
config_layout->addWidget(m_settle_frames_help);
|
||||
|
||||
auto* threshold_row = new QHBoxLayout;
|
||||
threshold_row->addWidget(new QLabel(tr("Min Draw Calls +:")));
|
||||
m_min_draw_call_spin = new QSpinBox;
|
||||
m_min_draw_call_spin->setRange(1, 1000000);
|
||||
m_min_draw_call_spin->setValue(1);
|
||||
m_min_draw_call_spin->setToolTip(
|
||||
tr("Minimum increase in draw calls versus the baseline required to keep a result."));
|
||||
threshold_row->addWidget(m_min_draw_call_spin);
|
||||
threshold_row->addWidget(new QLabel(tr("Min Triangles +:")));
|
||||
m_min_triangle_spin = new QSpinBox;
|
||||
m_min_triangle_spin->setRange(0, 100000000);
|
||||
m_min_triangle_spin->setValue(0);
|
||||
m_min_triangle_spin->setToolTip(
|
||||
tr("Minimum increase in drawn triangles versus the baseline required to keep a result."));
|
||||
threshold_row->addWidget(m_min_triangle_spin);
|
||||
threshold_row->addWidget(new QLabel(tr("Min Objects +:")));
|
||||
m_min_object_spin = new QSpinBox;
|
||||
m_min_object_spin->setRange(0, 1000000);
|
||||
m_min_object_spin->setValue(0);
|
||||
m_min_object_spin->setToolTip(
|
||||
tr("Minimum increase in drawn objects versus the baseline required to keep a result."));
|
||||
threshold_row->addWidget(m_min_object_spin);
|
||||
threshold_row->addStretch();
|
||||
config_layout->addLayout(threshold_row);
|
||||
|
||||
auto* filter_row = new QHBoxLayout;
|
||||
filter_row->addWidget(new QLabel(tr("Start Address:")));
|
||||
m_start_address_edit = new QLineEdit;
|
||||
m_start_address_edit->setPlaceholderText(QStringLiteral("0 or 80052FE8"));
|
||||
filter_row->addWidget(m_start_address_edit);
|
||||
filter_row->addWidget(new QLabel(tr("End Address:")));
|
||||
m_end_address_edit = new QLineEdit;
|
||||
m_end_address_edit->setPlaceholderText(QStringLiteral("0 or 80053000"));
|
||||
filter_row->addWidget(m_end_address_edit);
|
||||
filter_row->addWidget(new QLabel(tr("Symbol Filter:")));
|
||||
m_symbol_filter_edit = new QLineEdit;
|
||||
filter_row->addWidget(m_symbol_filter_edit, 1);
|
||||
config_layout->addLayout(filter_row);
|
||||
|
||||
auto* runtime_row = new QHBoxLayout;
|
||||
m_turbo_during_scan_check = new QCheckBox(tr("Turbo During Scan"));
|
||||
m_turbo_during_scan_check->setChecked(true);
|
||||
runtime_row->addWidget(m_turbo_during_scan_check);
|
||||
m_mute_audio_during_scan_check = new QCheckBox(tr("Mute Audio During Scan"));
|
||||
m_mute_audio_during_scan_check->setChecked(true);
|
||||
runtime_row->addWidget(m_mute_audio_during_scan_check);
|
||||
m_auto_capture_reference_state_check = new QCheckBox(tr("Auto-Capture Reference State"));
|
||||
m_auto_capture_reference_state_check->setChecked(true);
|
||||
runtime_row->addWidget(m_auto_capture_reference_state_check);
|
||||
runtime_row->addStretch();
|
||||
config_layout->addLayout(runtime_row);
|
||||
|
||||
auto* template_row = new QHBoxLayout;
|
||||
constexpr std::array<const char*, 4> labels = {"38600001 + BLR", "38600000 + BLR",
|
||||
"60000001 + BLR", "60000000 + BLR"};
|
||||
for (size_t i = 0; i < labels.size(); ++i)
|
||||
{
|
||||
m_template_checks[i] = new QCheckBox(QString::fromLatin1(labels[i]));
|
||||
m_template_checks[i]->setChecked(true);
|
||||
template_row->addWidget(m_template_checks[i]);
|
||||
}
|
||||
template_row->addStretch();
|
||||
config_layout->addLayout(template_row);
|
||||
main_layout->addWidget(config_group);
|
||||
|
||||
auto* controls_layout = new QHBoxLayout;
|
||||
m_start_btn = new QPushButton(tr("Start Scan"));
|
||||
m_restore_btn = new QPushButton(tr("Restore Session"));
|
||||
m_pause_btn = new QPushButton(tr("Pause"));
|
||||
m_stop_btn = new QPushButton(tr("Stop"));
|
||||
controls_layout->addWidget(m_start_btn);
|
||||
controls_layout->addWidget(m_restore_btn);
|
||||
controls_layout->addWidget(m_pause_btn);
|
||||
controls_layout->addWidget(m_stop_btn);
|
||||
controls_layout->addStretch();
|
||||
main_layout->addLayout(controls_layout);
|
||||
|
||||
m_progress_bar = new QProgressBar;
|
||||
m_progress_bar->setRange(0, 100);
|
||||
m_progress_bar->setValue(0);
|
||||
main_layout->addWidget(m_progress_bar);
|
||||
|
||||
m_status_label = new QLabel(tr("Idle"));
|
||||
main_layout->addWidget(m_status_label);
|
||||
|
||||
auto* results_group = new QGroupBox(tr("Results"));
|
||||
auto* results_layout = new QVBoxLayout;
|
||||
results_group->setLayout(results_layout);
|
||||
|
||||
m_results_table = new QTableWidget(0, 8);
|
||||
m_results_table->setHorizontalHeaderLabels(
|
||||
{tr("Symbol"), tr("Address"), tr("Template"), tr("DC Delta"), tr("Tri Delta"),
|
||||
tr("Obj Delta"), tr("Timeout"), tr("Screenshot")});
|
||||
m_results_table->setSelectionBehavior(QTableWidget::SelectRows);
|
||||
m_results_table->setSelectionMode(QTableWidget::ExtendedSelection);
|
||||
m_results_table->setEditTriggers(QTableWidget::NoEditTriggers);
|
||||
m_results_table->horizontalHeader()->setStretchLastSection(true);
|
||||
m_results_table->verticalHeader()->setVisible(false);
|
||||
results_layout->addWidget(m_results_table);
|
||||
|
||||
m_show_timeout_results_check = new QCheckBox(tr("Show Timeout Results"));
|
||||
m_show_timeout_results_check->setChecked(false);
|
||||
results_layout->addWidget(m_show_timeout_results_check);
|
||||
|
||||
auto* result_buttons = new QHBoxLayout;
|
||||
m_generate_ar_btn = new QPushButton(tr("Generate AR Code"));
|
||||
m_reset_results_btn = new QPushButton(tr("Reset Results"));
|
||||
result_buttons->addWidget(m_generate_ar_btn);
|
||||
result_buttons->addWidget(m_reset_results_btn);
|
||||
result_buttons->addStretch();
|
||||
results_layout->addLayout(result_buttons);
|
||||
main_layout->addWidget(results_group);
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::ConnectSignals()
|
||||
{
|
||||
connect(m_set_baseline_btn, &QPushButton::clicked, this,
|
||||
&CullingCodeFinderWidget::OnSetBaseline);
|
||||
connect(m_generate_map_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnGenerateMap);
|
||||
connect(m_start_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnStartScan);
|
||||
connect(m_restore_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnRestoreSession);
|
||||
connect(m_pause_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnPauseScan);
|
||||
connect(m_stop_btn, &QPushButton::clicked, this, &CullingCodeFinderWidget::OnStopScan);
|
||||
connect(m_generate_ar_btn, &QPushButton::clicked, this,
|
||||
&CullingCodeFinderWidget::OnGenerateARCode);
|
||||
connect(m_reset_results_btn, &QPushButton::clicked, this,
|
||||
&CullingCodeFinderWidget::OnResetResults);
|
||||
connect(m_show_timeout_results_check, &QCheckBox::toggled, this, [this] {
|
||||
UpdateResultsTable();
|
||||
UpdateButtonStates();
|
||||
});
|
||||
connect(m_browse_output_btn, &QPushButton::clicked, this,
|
||||
&CullingCodeFinderWidget::OnBrowseOutputFolder);
|
||||
connect(m_open_output_btn, &QPushButton::clicked, this,
|
||||
&CullingCodeFinderWidget::OnOpenOutputFolder);
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::RefreshStaticInfo()
|
||||
{
|
||||
const std::string game_id = SConfig::GetInstance().GetGameID();
|
||||
std::string map_path;
|
||||
PPCSymbolDB::FindMapFile(&map_path, nullptr);
|
||||
if (map_path.empty())
|
||||
{
|
||||
const std::string finder_map_path = CullingCodeFinder::GetInstance().GetMapPath();
|
||||
const std::string expected_suffix = game_id + ".map";
|
||||
if (!finder_map_path.empty() && !game_id.empty() && finder_map_path.ends_with(expected_suffix))
|
||||
map_path = finder_map_path;
|
||||
}
|
||||
|
||||
m_map_path_label->setText(map_path.empty() ? tr("Not found") : QString::fromStdString(map_path));
|
||||
|
||||
if (m_output_folder_edit->text().isEmpty() && !game_id.empty())
|
||||
{
|
||||
m_output_folder_edit->setText(
|
||||
QString::fromStdString(CullingCodeFinder::GetDefaultOutputFolder(game_id)));
|
||||
}
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::UpdateDisplay()
|
||||
{
|
||||
auto& finder = CullingCodeFinder::GetInstance();
|
||||
const auto stats = finder.GetLatestStats();
|
||||
m_live_draw_calls->setText(QString::number(stats.draw_calls));
|
||||
m_live_triangles->setText(QString::number(stats.triangles_drawn));
|
||||
m_live_prims->setText(QString::number(stats.prims));
|
||||
m_live_objects->setText(QString::number(stats.drawn_objects));
|
||||
|
||||
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning)
|
||||
finder.AdvanceScan(Core::System::GetInstance());
|
||||
|
||||
const auto baseline = finder.GetBaseline();
|
||||
m_baseline_label->setText(finder.HasBaseline() ?
|
||||
tr("DC: %1 | Tri: %2 | Prims: %3 | Obj: %4")
|
||||
.arg(baseline.draw_calls)
|
||||
.arg(baseline.triangles_drawn)
|
||||
.arg(baseline.prims)
|
||||
.arg(baseline.drawn_objects) :
|
||||
tr("Not set"));
|
||||
|
||||
m_progress_bar->setValue(static_cast<int>(finder.GetProgress() * 100.0f));
|
||||
m_status_label->setText(QString::fromStdString(finder.GetStatusText()));
|
||||
|
||||
const int result_count = finder.GetResultCount();
|
||||
if (result_count != m_last_result_count)
|
||||
{
|
||||
UpdateResultsTable();
|
||||
m_last_result_count = result_count;
|
||||
}
|
||||
|
||||
RefreshStaticInfo();
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnSetBaseline()
|
||||
{
|
||||
auto& finder = CullingCodeFinder::GetInstance();
|
||||
finder.SetBaseline();
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnStartScan()
|
||||
{
|
||||
CullingCodeFinder::ScanConfig config;
|
||||
bool start_address_ok = false;
|
||||
bool end_address_ok = false;
|
||||
const u32 parsed_start_address =
|
||||
ParseOptionalHexField(m_start_address_edit->text(), &start_address_ok);
|
||||
const u32 parsed_end_address = ParseOptionalHexField(m_end_address_edit->text(), &end_address_ok);
|
||||
if (!start_address_ok)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Start Address"),
|
||||
tr("Enter the start address as hexadecimal, for example 80052FE8."));
|
||||
return;
|
||||
}
|
||||
if (!end_address_ok)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid End Address"),
|
||||
tr("Enter the end address as hexadecimal, for example 80053000."));
|
||||
return;
|
||||
}
|
||||
if (parsed_start_address != 0 && parsed_end_address != 0 &&
|
||||
parsed_start_address > parsed_end_address)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Address Range"),
|
||||
tr("The start address must be lower than or equal to the end address."));
|
||||
return;
|
||||
}
|
||||
|
||||
config.savestate_slot = m_savestate_slot_spin->value();
|
||||
config.settle_frames = m_settle_frames_spin->value();
|
||||
config.min_draw_call_increase = m_min_draw_call_spin->value();
|
||||
config.min_triangle_increase = m_min_triangle_spin->value();
|
||||
config.min_object_increase = m_min_object_spin->value();
|
||||
config.candidate_limit = m_candidate_limit_spin->value();
|
||||
config.start_address = parsed_start_address;
|
||||
config.end_address = parsed_end_address;
|
||||
config.turbo_during_scan = m_turbo_during_scan_check->isChecked();
|
||||
config.mute_audio_during_scan = m_mute_audio_during_scan_check->isChecked();
|
||||
config.auto_capture_reference_state = m_auto_capture_reference_state_check->isChecked();
|
||||
config.symbol_filter = m_symbol_filter_edit->text().trimmed().toStdString();
|
||||
config.output_folder = m_output_folder_edit->text().trimmed().toStdString();
|
||||
|
||||
for (size_t i = 0; i < m_template_checks.size(); ++i)
|
||||
config.enabled_templates[i] = m_template_checks[i]->isChecked();
|
||||
|
||||
std::string error;
|
||||
if (!CullingCodeFinder::GetInstance().StartScan(Core::System::GetInstance(), std::move(config),
|
||||
&error))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Unable to Start Scan"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
m_last_result_count = -1;
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnRestoreSession()
|
||||
{
|
||||
const QString output_folder = m_output_folder_edit->text().trimmed();
|
||||
if (output_folder.isEmpty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("No Output Folder"),
|
||||
tr("Select the scan output folder that contains manifest.ini first."));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
std::string restored_message;
|
||||
auto& finder = CullingCodeFinder::GetInstance();
|
||||
if (!finder.RestoreSession(Core::System::GetInstance(), output_folder.toStdString(), &error,
|
||||
&restored_message))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Unable to Restore Session"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyConfigToWidgets();
|
||||
m_last_result_count = -1;
|
||||
finder.AdvanceScan(Core::System::GetInstance());
|
||||
UpdateDisplay();
|
||||
|
||||
QMessageBox::information(this, tr("Session Restored"),
|
||||
QString::fromStdString(restored_message));
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnGenerateMap()
|
||||
{
|
||||
std::string error;
|
||||
if (!CullingCodeFinder::GetInstance().GenerateSymbolMap(Core::System::GetInstance(), &error))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Unable to Generate Map"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
emit Host::GetInstance()->PPCSymbolsChanged();
|
||||
RefreshStaticInfo();
|
||||
QMessageBox::information(this, tr("Map Generated"),
|
||||
tr("Rebuilt and saved the writable symbol map for the running game."));
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnPauseScan()
|
||||
{
|
||||
auto& finder = CullingCodeFinder::GetInstance();
|
||||
if (finder.GetState() == CullingCodeFinder::ScanState::Scanning)
|
||||
{
|
||||
finder.PauseScan();
|
||||
finder.AdvanceScan(Core::System::GetInstance());
|
||||
}
|
||||
else if (finder.GetState() == CullingCodeFinder::ScanState::Paused)
|
||||
{
|
||||
finder.ResumeScan();
|
||||
}
|
||||
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnStopScan()
|
||||
{
|
||||
auto& finder = CullingCodeFinder::GetInstance();
|
||||
finder.StopScan();
|
||||
finder.AdvanceScan(Core::System::GetInstance());
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnGenerateARCode()
|
||||
{
|
||||
const std::string game_id = SConfig::GetInstance().GetGameID();
|
||||
if (game_id.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("No Game Running"),
|
||||
tr("Boot a game before generating Action Replay codes."));
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndexList selected_rows = m_results_table->selectionModel()->selectedRows();
|
||||
if (selected_rows.empty())
|
||||
{
|
||||
QMessageBox::information(this, tr("No Results Selected"),
|
||||
tr("Select one or more results first."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto results = CullingCodeFinder::GetInstance().GetResults();
|
||||
std::vector<int> row_indices;
|
||||
row_indices.reserve(selected_rows.size());
|
||||
for (const QModelIndex& index : selected_rows)
|
||||
row_indices.push_back(index.row());
|
||||
std::ranges::sort(row_indices);
|
||||
|
||||
std::vector<ActionReplay::ARCode> generated_codes;
|
||||
generated_codes.reserve(row_indices.size());
|
||||
QString clipboard_text;
|
||||
|
||||
for (int row : row_indices)
|
||||
{
|
||||
if (row < 0 || row >= static_cast<int>(m_visible_result_indices.size()))
|
||||
continue;
|
||||
|
||||
const int result_index = m_visible_result_indices[static_cast<size_t>(row)];
|
||||
if (result_index < 0 || result_index >= static_cast<int>(results.size()))
|
||||
continue;
|
||||
|
||||
const auto& result = results[static_cast<size_t>(result_index)];
|
||||
const auto lines = CullingCodeFinder::BuildARCodeLines(result.address, result.template_kind);
|
||||
|
||||
ActionReplay::ARCode code;
|
||||
code.name =
|
||||
fmt::format("VR Culling - {} [{:08X}] {}", result.symbol_name, result.address,
|
||||
CullingCodeFinder::GetPatchTemplateName(result.template_kind));
|
||||
code.enabled = true;
|
||||
code.default_enabled = false;
|
||||
code.user_defined = true;
|
||||
|
||||
for (const std::string& line : lines)
|
||||
{
|
||||
const auto parsed = ActionReplay::DeserializeLine(line);
|
||||
if (const auto* entry = std::get_if<ActionReplay::AREntry>(&parsed))
|
||||
code.ops.push_back(*entry);
|
||||
}
|
||||
|
||||
if (code.ops.size() != lines.size())
|
||||
continue;
|
||||
|
||||
generated_codes.push_back(std::move(code));
|
||||
|
||||
if (!clipboard_text.isEmpty())
|
||||
clipboard_text.append(QStringLiteral("\n\n"));
|
||||
clipboard_text.append(QString::fromStdString(result.ar_code_text));
|
||||
}
|
||||
|
||||
if (generated_codes.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Unable to Generate Codes"),
|
||||
tr("The selected results did not produce valid AR code lines."));
|
||||
return;
|
||||
}
|
||||
|
||||
Common::IniFile game_ini_local;
|
||||
const Common::IniFile game_ini_default =
|
||||
SConfig::LoadDefaultGameIni(game_id, SConfig::GetInstance().GetRevision());
|
||||
const std::string ini_path =
|
||||
std::string(File::GetUserPath(D_GAMESETTINGS_IDX)).append(game_id).append(".ini");
|
||||
game_ini_local.Load(ini_path);
|
||||
|
||||
std::vector<ActionReplay::ARCode> codes = ActionReplay::LoadCodes(game_ini_default, game_ini_local);
|
||||
|
||||
for (ActionReplay::ARCode& code : generated_codes)
|
||||
{
|
||||
std::erase_if(codes, [&](const ActionReplay::ARCode& existing_code) {
|
||||
return existing_code.user_defined && existing_code.name == code.name;
|
||||
});
|
||||
codes.push_back(std::move(code));
|
||||
}
|
||||
|
||||
ActionReplay::SaveCodes(&game_ini_local, codes);
|
||||
if (!game_ini_local.Save(ini_path))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Unable to Save Codes"),
|
||||
tr("Failed to save generated codes to the local game INI."));
|
||||
return;
|
||||
}
|
||||
|
||||
ActionReplay::ApplyCodes(codes, game_id, SConfig::GetInstance().GetRevision());
|
||||
QGuiApplication::clipboard()->setText(clipboard_text);
|
||||
|
||||
QMessageBox::information(this, tr("AR Codes Generated"),
|
||||
tr("Saved %1 code(s) and copied the generated text to the clipboard.")
|
||||
.arg(generated_codes.size()));
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnBrowseOutputFolder()
|
||||
{
|
||||
const QString current_path = m_output_folder_edit->text().trimmed();
|
||||
const QString selected_path =
|
||||
QFileDialog::getExistingDirectory(this, tr("Select Output Folder"), current_path);
|
||||
if (!selected_path.isEmpty())
|
||||
m_output_folder_edit->setText(selected_path);
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnOpenOutputFolder()
|
||||
{
|
||||
const QString output_path = m_output_folder_edit->text().trimmed();
|
||||
if (output_path.isEmpty())
|
||||
return;
|
||||
|
||||
File::CreateFullPath(output_path.toStdString() + DIR_SEP);
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(output_path));
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::OnResetResults()
|
||||
{
|
||||
auto& finder = CullingCodeFinder::GetInstance();
|
||||
std::string error;
|
||||
finder.ClearResults(&error);
|
||||
if (!error.empty())
|
||||
{
|
||||
QMessageBox::warning(this, tr("Unable to Reset Results"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
m_last_result_count = -1;
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::UpdateResultsTable()
|
||||
{
|
||||
const auto results = CullingCodeFinder::GetInstance().GetResults();
|
||||
const bool show_timeouts =
|
||||
m_show_timeout_results_check && m_show_timeout_results_check->isChecked();
|
||||
|
||||
m_visible_result_indices.clear();
|
||||
m_visible_result_indices.reserve(results.size());
|
||||
for (int i = 0; i < static_cast<int>(results.size()); ++i)
|
||||
{
|
||||
if (!show_timeouts && results[static_cast<size_t>(i)].caused_timeout)
|
||||
continue;
|
||||
m_visible_result_indices.push_back(i);
|
||||
}
|
||||
|
||||
QSignalBlocker blocker(m_results_table);
|
||||
m_results_table->clearSelection();
|
||||
m_results_table->setRowCount(static_cast<int>(m_visible_result_indices.size()));
|
||||
|
||||
for (int row = 0; row < static_cast<int>(m_visible_result_indices.size()); ++row)
|
||||
{
|
||||
const auto& result = results[static_cast<size_t>(m_visible_result_indices[row])];
|
||||
auto* symbol_item = new QTableWidgetItem(QString::fromStdString(result.symbol_name));
|
||||
auto* address_item = new QTableWidgetItem(QString::asprintf("%08X", result.address));
|
||||
auto* template_item = new QTableWidgetItem(
|
||||
QString::fromStdString(CullingCodeFinder::GetPatchTemplateName(result.template_kind)));
|
||||
auto* draw_call_item = new QTableWidgetItem(QString::number(result.draw_call_delta));
|
||||
auto* triangle_item = new QTableWidgetItem(QString::number(result.triangle_delta));
|
||||
auto* object_item = new QTableWidgetItem(QString::number(result.object_delta));
|
||||
auto* timeout_item = new QTableWidgetItem(result.caused_timeout ? tr("Yes") : tr("No"));
|
||||
auto* screenshot_item = new QTableWidgetItem(PathLeaf(result.screenshot_path));
|
||||
|
||||
address_item->setTextAlignment(Qt::AlignCenter);
|
||||
draw_call_item->setTextAlignment(Qt::AlignCenter);
|
||||
triangle_item->setTextAlignment(Qt::AlignCenter);
|
||||
object_item->setTextAlignment(Qt::AlignCenter);
|
||||
timeout_item->setTextAlignment(Qt::AlignCenter);
|
||||
|
||||
m_results_table->setItem(row, 0, symbol_item);
|
||||
m_results_table->setItem(row, 1, address_item);
|
||||
m_results_table->setItem(row, 2, template_item);
|
||||
m_results_table->setItem(row, 3, draw_call_item);
|
||||
m_results_table->setItem(row, 4, triangle_item);
|
||||
m_results_table->setItem(row, 5, object_item);
|
||||
m_results_table->setItem(row, 6, timeout_item);
|
||||
m_results_table->setItem(row, 7, screenshot_item);
|
||||
}
|
||||
|
||||
m_results_table->resizeColumnsToContents();
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::UpdateButtonStates()
|
||||
{
|
||||
const auto state = CullingCodeFinder::GetInstance().GetState();
|
||||
const bool actively_scanning = state == CullingCodeFinder::ScanState::Scanning;
|
||||
const bool paused = state == CullingCodeFinder::ScanState::Paused;
|
||||
const bool busy = actively_scanning || paused;
|
||||
|
||||
m_set_baseline_btn->setEnabled(!busy);
|
||||
m_generate_map_btn->setEnabled(!busy);
|
||||
m_savestate_slot_spin->setEnabled(!busy);
|
||||
m_settle_frames_spin->setEnabled(!busy);
|
||||
m_min_draw_call_spin->setEnabled(!busy);
|
||||
m_min_triangle_spin->setEnabled(!busy);
|
||||
m_min_object_spin->setEnabled(!busy);
|
||||
m_candidate_limit_spin->setEnabled(!busy);
|
||||
m_start_address_edit->setEnabled(!busy);
|
||||
m_end_address_edit->setEnabled(!busy);
|
||||
m_symbol_filter_edit->setEnabled(!busy);
|
||||
m_turbo_during_scan_check->setEnabled(!busy);
|
||||
m_mute_audio_during_scan_check->setEnabled(!busy);
|
||||
m_auto_capture_reference_state_check->setEnabled(!busy);
|
||||
m_output_folder_edit->setEnabled(!busy);
|
||||
m_browse_output_btn->setEnabled(!busy);
|
||||
for (QCheckBox* checkbox : m_template_checks)
|
||||
{
|
||||
if (checkbox)
|
||||
checkbox->setEnabled(!busy);
|
||||
}
|
||||
|
||||
m_start_btn->setEnabled(!busy);
|
||||
m_restore_btn->setEnabled(!busy && !m_output_folder_edit->text().trimmed().isEmpty());
|
||||
m_pause_btn->setEnabled(actively_scanning || paused);
|
||||
m_pause_btn->setText(paused ? tr("Resume") : tr("Pause"));
|
||||
m_stop_btn->setEnabled(busy);
|
||||
m_show_timeout_results_check->setEnabled(true);
|
||||
m_generate_ar_btn->setEnabled(m_results_table->rowCount() > 0);
|
||||
m_reset_results_btn->setEnabled(!busy && !CullingCodeFinder::GetInstance().GetResults().empty());
|
||||
m_open_output_btn->setEnabled(!m_output_folder_edit->text().trimmed().isEmpty());
|
||||
}
|
||||
|
||||
void CullingCodeFinderWidget::ApplyConfigToWidgets()
|
||||
{
|
||||
const auto config = CullingCodeFinder::GetInstance().GetConfig();
|
||||
|
||||
m_savestate_slot_spin->setValue(config.savestate_slot);
|
||||
m_settle_frames_spin->setValue(config.settle_frames);
|
||||
m_min_draw_call_spin->setValue(config.min_draw_call_increase);
|
||||
m_min_triangle_spin->setValue(config.min_triangle_increase);
|
||||
m_min_object_spin->setValue(config.min_object_increase);
|
||||
m_candidate_limit_spin->setValue(config.candidate_limit);
|
||||
m_start_address_edit->setText(config.start_address == 0 ?
|
||||
QString{} :
|
||||
QString::asprintf("%08X", config.start_address));
|
||||
m_end_address_edit->setText(config.end_address == 0 ?
|
||||
QString{} :
|
||||
QString::asprintf("%08X", config.end_address));
|
||||
m_symbol_filter_edit->setText(QString::fromStdString(config.symbol_filter));
|
||||
m_turbo_during_scan_check->setChecked(config.turbo_during_scan);
|
||||
m_mute_audio_during_scan_check->setChecked(config.mute_audio_during_scan);
|
||||
m_auto_capture_reference_state_check->setChecked(config.auto_capture_reference_state);
|
||||
m_output_folder_edit->setText(QString::fromStdString(config.output_folder));
|
||||
|
||||
for (size_t i = 0; i < m_template_checks.size(); ++i)
|
||||
{
|
||||
if (m_template_checks[i])
|
||||
m_template_checks[i]->setChecked(config.enabled_templates[i]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QProgressBar;
|
||||
class QSpinBox;
|
||||
class QTableWidget;
|
||||
class QTimer;
|
||||
|
||||
class CullingCodeFinderWidget : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CullingCodeFinderWidget(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void CreateWidgets();
|
||||
void ConnectSignals();
|
||||
void RefreshStaticInfo();
|
||||
void UpdateDisplay();
|
||||
void OnSetBaseline();
|
||||
void OnStartScan();
|
||||
void OnRestoreSession();
|
||||
void OnPauseScan();
|
||||
void OnStopScan();
|
||||
void OnGenerateMap();
|
||||
void OnGenerateARCode();
|
||||
void OnBrowseOutputFolder();
|
||||
void OnOpenOutputFolder();
|
||||
void OnResetResults();
|
||||
void UpdateResultsTable();
|
||||
void UpdateButtonStates();
|
||||
void ApplyConfigToWidgets();
|
||||
|
||||
QLabel* m_live_draw_calls = nullptr;
|
||||
QLabel* m_live_triangles = nullptr;
|
||||
QLabel* m_live_prims = nullptr;
|
||||
QLabel* m_live_objects = nullptr;
|
||||
|
||||
QLabel* m_baseline_label = nullptr;
|
||||
QPushButton* m_set_baseline_btn = nullptr;
|
||||
|
||||
QLabel* m_map_path_label = nullptr;
|
||||
QPushButton* m_generate_map_btn = nullptr;
|
||||
QLineEdit* m_output_folder_edit = nullptr;
|
||||
QPushButton* m_browse_output_btn = nullptr;
|
||||
QPushButton* m_open_output_btn = nullptr;
|
||||
|
||||
QSpinBox* m_savestate_slot_spin = nullptr;
|
||||
QSpinBox* m_settle_frames_spin = nullptr;
|
||||
QLabel* m_settle_frames_help = nullptr;
|
||||
QSpinBox* m_min_draw_call_spin = nullptr;
|
||||
QSpinBox* m_min_triangle_spin = nullptr;
|
||||
QSpinBox* m_min_object_spin = nullptr;
|
||||
QSpinBox* m_candidate_limit_spin = nullptr;
|
||||
QLineEdit* m_start_address_edit = nullptr;
|
||||
QLineEdit* m_end_address_edit = nullptr;
|
||||
QLineEdit* m_symbol_filter_edit = nullptr;
|
||||
QCheckBox* m_turbo_during_scan_check = nullptr;
|
||||
QCheckBox* m_mute_audio_during_scan_check = nullptr;
|
||||
QCheckBox* m_auto_capture_reference_state_check = nullptr;
|
||||
std::array<QCheckBox*, 4> m_template_checks{};
|
||||
|
||||
QPushButton* m_start_btn = nullptr;
|
||||
QPushButton* m_restore_btn = nullptr;
|
||||
QPushButton* m_pause_btn = nullptr;
|
||||
QPushButton* m_stop_btn = nullptr;
|
||||
QProgressBar* m_progress_bar = nullptr;
|
||||
QLabel* m_status_label = nullptr;
|
||||
|
||||
QTableWidget* m_results_table = nullptr;
|
||||
QCheckBox* m_show_timeout_results_check = nullptr;
|
||||
QPushButton* m_generate_ar_btn = nullptr;
|
||||
QPushButton* m_reset_results_btn = nullptr;
|
||||
std::vector<int> m_visible_result_indices;
|
||||
int m_last_result_count = -1;
|
||||
|
||||
QTimer* m_update_timer = nullptr;
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <QCloseEvent>
|
||||
#include <QDialog>
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QTimer;
|
||||
class QVBoxLayout;
|
||||
class QWidget;
|
||||
|
||||
class ShaderHunterWidget : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ShaderHunterWidget(QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void OverridesChanged();
|
||||
|
||||
private:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void CreateWidgets();
|
||||
void ConnectSignals();
|
||||
void UpdateDisplay();
|
||||
void ShowDrawCallDialog();
|
||||
void SetSelectedDrawCallRange(int start, int end, int total);
|
||||
void SetSelectedTextureHashes(const std::vector<uint64_t>& hashes);
|
||||
void SaveCurrentShader();
|
||||
void DumpCurrentShader();
|
||||
void ShowTexturesDialog();
|
||||
|
||||
QVBoxLayout* m_main_layout = nullptr;
|
||||
QCheckBox* m_enable_checkbox;
|
||||
QComboBox* m_hunting_option_combo;
|
||||
QCheckBox* m_debug_log_checkbox;
|
||||
QComboBox* m_type_combo;
|
||||
QLabel* m_hash_label;
|
||||
QLabel* m_position_label;
|
||||
QPushButton* m_prev_button;
|
||||
QPushButton* m_next_button;
|
||||
QLineEdit* m_shader_name_edit;
|
||||
QComboBox* m_handling_combo;
|
||||
QLabel* m_units_per_meter_label;
|
||||
QDoubleSpinBox* m_units_per_meter_spin;
|
||||
QPushButton* m_save_button;
|
||||
QPushButton* m_dump_button;
|
||||
QPushButton* m_textures_button;
|
||||
QPushButton* m_draw_calls_button;
|
||||
QLabel* m_selected_draw_call_label;
|
||||
QComboBox* m_texture_filter_mode_combo;
|
||||
QLabel* m_selected_texture_label;
|
||||
QTimer* m_update_timer;
|
||||
int m_saved_element_start = -1;
|
||||
int m_saved_element_end = -1;
|
||||
int m_saved_element_total = 0;
|
||||
std::vector<uint64_t> m_saved_texture_filters;
|
||||
};
|
||||
@@ -36,13 +36,19 @@
|
||||
#include "UICommon/DiscordPresence.h"
|
||||
|
||||
#include "VideoCommon/AbstractGfx.h"
|
||||
#include "VideoCommon/CullingCodeFinder.h"
|
||||
#include "VideoCommon/Fifo.h"
|
||||
#include "VideoCommon/Present.h"
|
||||
#include "VideoCommon/VideoConfig.h"
|
||||
|
||||
Host::Host()
|
||||
{
|
||||
State::SetOnAfterLoadCallback([] { Host_UpdateDisasmDialog(); });
|
||||
State::SetOnAfterLoadCallback([] {
|
||||
auto& finder = CullingCodeFinder::GetInstance();
|
||||
finder.NotifySavestateLoaded();
|
||||
finder.AdvanceScan(Core::System::GetInstance());
|
||||
Host_UpdateDisasmDialog();
|
||||
});
|
||||
}
|
||||
|
||||
Host::~Host()
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "Core/Config/GraphicsSettings.h"
|
||||
#include "Core/Config/MainSettings.h"
|
||||
#include "Core/Config/UISettings.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/FreeLookManager.h"
|
||||
#include "Core/HotkeyManager.h"
|
||||
@@ -37,12 +38,19 @@
|
||||
#include "DolphinQt/Settings.h"
|
||||
|
||||
#include "InputCommon/ControlReference/ControlReference.h"
|
||||
#include "VideoCommon/CullingCodeFinder.h"
|
||||
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||
|
||||
#include "VideoCommon/OnScreenDisplay.h"
|
||||
#include "VideoCommon/ShaderHunter.h"
|
||||
#include "VideoCommon/VideoConfig.h"
|
||||
#ifdef ENABLE_VR
|
||||
#include "VideoCommon/VR/OpenXRManager.h"
|
||||
#endif
|
||||
|
||||
constexpr const char* DUBOIS_ALGORITHM_SHADER = "dubois";
|
||||
constexpr float HOTKEY_VR_UNITS_PER_METER_MIN = 0.01f;
|
||||
constexpr float HOTKEY_VR_UNITS_PER_METER_MAX = 1000.0f;
|
||||
|
||||
HotkeyScheduler::HotkeyScheduler() : m_stop_requested(false)
|
||||
{
|
||||
@@ -483,15 +491,329 @@ void HotkeyScheduler::Run()
|
||||
if (IsHotkey(HK_TOGGLE_TEXTURES))
|
||||
Config::SetCurrent(Config::GFX_HIRES_TEXTURES, !Config::Get(Config::GFX_HIRES_TEXTURES));
|
||||
|
||||
Core::SetIsThrottlerTempDisabled(IsHotkey(HK_TOGGLE_THROTTLE, true));
|
||||
// VR
|
||||
auto ToggleVRSetting = [](const auto& setting, const char* label) {
|
||||
const bool new_value = !Config::Get(setting);
|
||||
Config::SetCurrent(setting, new_value);
|
||||
OSD::AddMessage(fmt::format("{}: {}", label, new_value ? "Enabled" : "Disabled"));
|
||||
};
|
||||
|
||||
if (IsHotkey(HK_TOGGLE_THROTTLE, true) && !Config::Get(Config::MAIN_AUDIO_MUTED) &&
|
||||
Config::Get(Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT))
|
||||
auto AdjustVRFloatSetting = [](const auto& setting, const float delta, const float min_value,
|
||||
const float max_value, const char* label, const int precision) {
|
||||
const float current = Config::Get(setting);
|
||||
const float new_value = std::clamp(current + delta, min_value, max_value);
|
||||
Config::SetCurrent(setting, new_value);
|
||||
OSD::AddMessage(fmt::format("{}: {:.{}f}", label, new_value, precision));
|
||||
};
|
||||
|
||||
if (IsHotkey(HK_VR_TOGGLE_OPENXR))
|
||||
ToggleVRSetting(Config::GFX_VR_ENABLE_OPENXR, "OpenXR");
|
||||
|
||||
if (IsHotkey(HK_VR_RESET_POSITION))
|
||||
{
|
||||
#ifdef ENABLE_VR
|
||||
if (VR::g_openxr)
|
||||
{
|
||||
VR::g_openxr->RequestRecenter();
|
||||
OSD::AddMessage("VR Position: Recenter requested");
|
||||
}
|
||||
else
|
||||
{
|
||||
OSD::AddMessage("VR Position: OpenXR is not active");
|
||||
}
|
||||
#else
|
||||
OSD::AddMessage("VR Position: VR backend not enabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_DECREASE_UNITS_PER_METER))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_UNITS_PER_METER, -Config::GFX_VR_UNITS_PER_METER_STEP,
|
||||
HOTKEY_VR_UNITS_PER_METER_MIN, HOTKEY_VR_UNITS_PER_METER_MAX,
|
||||
"Units Per Meter", 2);
|
||||
}
|
||||
if (IsHotkey(HK_VR_INCREASE_UNITS_PER_METER))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_UNITS_PER_METER, Config::GFX_VR_UNITS_PER_METER_STEP,
|
||||
HOTKEY_VR_UNITS_PER_METER_MIN, HOTKEY_VR_UNITS_PER_METER_MAX,
|
||||
"Units Per Meter", 2);
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_DECREASE_LEAN_BACK_ANGLE))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_LEAN_BACK_ANGLE, -Config::GFX_VR_LEAN_BACK_ANGLE_STEP,
|
||||
Config::GFX_VR_LEAN_BACK_ANGLE_MIN,
|
||||
Config::GFX_VR_LEAN_BACK_ANGLE_MAX, "Lean Back Angle", 1);
|
||||
}
|
||||
if (IsHotkey(HK_VR_INCREASE_LEAN_BACK_ANGLE))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_LEAN_BACK_ANGLE, Config::GFX_VR_LEAN_BACK_ANGLE_STEP,
|
||||
Config::GFX_VR_LEAN_BACK_ANGLE_MIN,
|
||||
Config::GFX_VR_LEAN_BACK_ANGLE_MAX, "Lean Back Angle", 1);
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_DECREASE_CAMERA_FORWARD))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_CAMERA_FORWARD, -Config::GFX_VR_CAMERA_FORWARD_STEP,
|
||||
Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX,
|
||||
"Camera Forward", 2);
|
||||
}
|
||||
if (IsHotkey(HK_VR_INCREASE_CAMERA_FORWARD))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_CAMERA_FORWARD, Config::GFX_VR_CAMERA_FORWARD_STEP,
|
||||
Config::GFX_VR_CAMERA_FORWARD_MIN, Config::GFX_VR_CAMERA_FORWARD_MAX,
|
||||
"Camera Forward", 2);
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_TOGGLE_VIRTUAL_SCREEN))
|
||||
ToggleVRSetting(Config::GFX_VR_VIRTUAL_SCREEN, "Virtual Screen");
|
||||
|
||||
if (IsHotkey(HK_VR_DECREASE_SCREEN_DISTANCE))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_SCREEN_DISTANCE, -Config::GFX_VR_SCREEN_DISTANCE_STEP,
|
||||
Config::GFX_VR_SCREEN_DISTANCE_MIN,
|
||||
Config::GFX_VR_SCREEN_DISTANCE_MAX, "Screen Distance", 2);
|
||||
}
|
||||
if (IsHotkey(HK_VR_INCREASE_SCREEN_DISTANCE))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_SCREEN_DISTANCE, Config::GFX_VR_SCREEN_DISTANCE_STEP,
|
||||
Config::GFX_VR_SCREEN_DISTANCE_MIN,
|
||||
Config::GFX_VR_SCREEN_DISTANCE_MAX, "Screen Distance", 2);
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_DECREASE_SCREEN_SIZE))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_SCREEN_SIZE, -Config::GFX_VR_SCREEN_SIZE_STEP,
|
||||
Config::GFX_VR_SCREEN_SIZE_MIN, Config::GFX_VR_SCREEN_SIZE_MAX,
|
||||
"Screen Size", 2);
|
||||
}
|
||||
if (IsHotkey(HK_VR_INCREASE_SCREEN_SIZE))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_SCREEN_SIZE, Config::GFX_VR_SCREEN_SIZE_STEP,
|
||||
Config::GFX_VR_SCREEN_SIZE_MIN, Config::GFX_VR_SCREEN_SIZE_MAX,
|
||||
"Screen Size", 2);
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_DECREASE_SCREEN_CURVATURE))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_HEAD_LOCKED_CURVATURE,
|
||||
-Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP,
|
||||
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN,
|
||||
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX, "Screen Curvature", 2);
|
||||
}
|
||||
if (IsHotkey(HK_VR_INCREASE_SCREEN_CURVATURE))
|
||||
{
|
||||
AdjustVRFloatSetting(Config::GFX_VR_HEAD_LOCKED_CURVATURE,
|
||||
Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP,
|
||||
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MIN,
|
||||
Config::GFX_VR_HEAD_LOCKED_CURVATURE_MAX, "Screen Curvature", 2);
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_TOGGLE_DONT_CLEAR_SCREEN))
|
||||
ToggleVRSetting(Config::GFX_VR_DONT_CLEAR_SCREEN, "Don't Clear Screen");
|
||||
|
||||
if (IsHotkey(HK_VR_TOGGLE_FORCE_VBI))
|
||||
ToggleVRSetting(Config::GFX_VR_AUTO_VBI_FROM_HMD, "Force VBI");
|
||||
|
||||
if (IsHotkey(HK_VR_TOGGLE_REMOVE_CINEMATIC_BARS))
|
||||
ToggleVRSetting(Config::GFX_VR_REMOVE_BARS, "Remove Cinematic Bars");
|
||||
|
||||
// VR Shader Hunting
|
||||
auto& shader_hunter = ShaderHunter::GetInstance();
|
||||
static ShaderHunter::HandlingType shader_hotkey_handling = ShaderHunter::HandlingType::Skip;
|
||||
auto ShaderTypeLabel = [](ShaderHunter::ShaderType type) {
|
||||
switch (type)
|
||||
{
|
||||
case ShaderHunter::ShaderType::Pixel:
|
||||
return "Pixel";
|
||||
case ShaderHunter::ShaderType::Vertex:
|
||||
return "Vertex";
|
||||
case ShaderHunter::ShaderType::Geometry:
|
||||
return "Geometry";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
auto HandlingTypeLabel = [](ShaderHunter::HandlingType type) {
|
||||
switch (type)
|
||||
{
|
||||
case ShaderHunter::HandlingType::Skip:
|
||||
return "Skip";
|
||||
case ShaderHunter::HandlingType::Screen:
|
||||
return "Screen";
|
||||
case ShaderHunter::HandlingType::HeadLocked:
|
||||
return "Head Locked";
|
||||
case ShaderHunter::HandlingType::Fullscreen:
|
||||
return "Fullscreen";
|
||||
case ShaderHunter::HandlingType::FullscreenMono:
|
||||
return "Fullscreen";
|
||||
case ShaderHunter::HandlingType::Flag:
|
||||
return "Flag";
|
||||
case ShaderHunter::HandlingType::UnitsPerMeter:
|
||||
return "Units Per Meter";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
auto ShowSelectedShader = [&shader_hunter, ShaderTypeLabel]() {
|
||||
const int pos = shader_hunter.GetSelectedPosition();
|
||||
const int total = shader_hunter.GetTotalCount();
|
||||
const u64 hash = shader_hunter.GetSelectedHash();
|
||||
if (pos < 0 || total <= 0 || hash == ~0ULL)
|
||||
{
|
||||
OSD::AddMessage(fmt::format("Shader: {} (none)",
|
||||
ShaderTypeLabel(shader_hunter.GetActiveType())));
|
||||
return;
|
||||
}
|
||||
|
||||
OSD::AddMessage(fmt::format("Shader: {} {:08x} ({}/{})",
|
||||
ShaderTypeLabel(shader_hunter.GetActiveType()),
|
||||
static_cast<u32>(hash), pos + 1, total));
|
||||
};
|
||||
|
||||
if (IsHotkey(HK_VR_SHADER_TOGGLE_HUNTING))
|
||||
{
|
||||
const bool enabled = !shader_hunter.IsEnabled();
|
||||
shader_hunter.SetEnabled(enabled);
|
||||
OSD::AddMessage(fmt::format("Shader Hunting: {}", enabled ? "Enabled" : "Disabled"));
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_SHADER_CYCLE_HUNTING_OPTION))
|
||||
{
|
||||
const auto new_option = shader_hunter.GetHuntingOption() == ShaderHunter::HuntingOption::Skip ?
|
||||
ShaderHunter::HuntingOption::Pink :
|
||||
ShaderHunter::HuntingOption::Skip;
|
||||
shader_hunter.SetHuntingOption(new_option);
|
||||
OSD::AddMessage(fmt::format("Hunting Option: {}",
|
||||
new_option == ShaderHunter::HuntingOption::Pink ? "Pink" :
|
||||
"Skip"));
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_SHADER_CYCLE_TYPE))
|
||||
{
|
||||
const int next_type =
|
||||
(static_cast<int>(shader_hunter.GetActiveType()) + 1) %
|
||||
static_cast<int>(ShaderHunter::ShaderType::Count);
|
||||
shader_hunter.SetActiveType(static_cast<ShaderHunter::ShaderType>(next_type));
|
||||
OSD::AddMessage(
|
||||
fmt::format("Shader Type: {}", ShaderTypeLabel(shader_hunter.GetActiveType())));
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_SHADER_CYCLE_HANDLING))
|
||||
{
|
||||
switch (shader_hotkey_handling)
|
||||
{
|
||||
case ShaderHunter::HandlingType::Skip:
|
||||
shader_hotkey_handling = ShaderHunter::HandlingType::Screen;
|
||||
break;
|
||||
case ShaderHunter::HandlingType::Screen:
|
||||
shader_hotkey_handling = ShaderHunter::HandlingType::HeadLocked;
|
||||
break;
|
||||
case ShaderHunter::HandlingType::HeadLocked:
|
||||
shader_hotkey_handling = ShaderHunter::HandlingType::Fullscreen;
|
||||
break;
|
||||
case ShaderHunter::HandlingType::Fullscreen:
|
||||
shader_hotkey_handling = ShaderHunter::HandlingType::Flag;
|
||||
break;
|
||||
case ShaderHunter::HandlingType::FullscreenMono:
|
||||
shader_hotkey_handling = ShaderHunter::HandlingType::Flag;
|
||||
break;
|
||||
case ShaderHunter::HandlingType::Flag:
|
||||
shader_hotkey_handling = ShaderHunter::HandlingType::UnitsPerMeter;
|
||||
break;
|
||||
case ShaderHunter::HandlingType::UnitsPerMeter:
|
||||
default:
|
||||
shader_hotkey_handling = ShaderHunter::HandlingType::Skip;
|
||||
break;
|
||||
}
|
||||
OSD::AddMessage(
|
||||
fmt::format("Override Handling: {}", HandlingTypeLabel(shader_hotkey_handling)));
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_SHADER_PREV))
|
||||
{
|
||||
shader_hunter.PrevShader();
|
||||
ShowSelectedShader();
|
||||
}
|
||||
if (IsHotkey(HK_VR_SHADER_NEXT))
|
||||
{
|
||||
shader_hunter.NextShader();
|
||||
ShowSelectedShader();
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_SHADER_PREV_TEXTURE_HASH))
|
||||
{
|
||||
shader_hunter.PrevTextureHash();
|
||||
const u64 texture_hash = shader_hunter.GetSelectedTextureHash();
|
||||
if (texture_hash == 0)
|
||||
OSD::AddMessage("Texture Hash: (none)");
|
||||
else
|
||||
OSD::AddMessage(
|
||||
fmt::format("Texture Hash: {:016x}", static_cast<unsigned long long>(texture_hash)));
|
||||
}
|
||||
if (IsHotkey(HK_VR_SHADER_NEXT_TEXTURE_HASH))
|
||||
{
|
||||
shader_hunter.NextTextureHash();
|
||||
const u64 texture_hash = shader_hunter.GetSelectedTextureHash();
|
||||
if (texture_hash == 0)
|
||||
OSD::AddMessage("Texture Hash: (none)");
|
||||
else
|
||||
OSD::AddMessage(
|
||||
fmt::format("Texture Hash: {:016x}", static_cast<unsigned long long>(texture_hash)));
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_SHADER_TOGGLE_TEXTURE_HASH))
|
||||
{
|
||||
const bool enabled = shader_hunter.ToggleSelectedTextureHashFilter();
|
||||
const u64 texture_hash = shader_hunter.GetSelectedTextureHash();
|
||||
if (texture_hash == 0)
|
||||
{
|
||||
OSD::AddMessage("Texture Hash: (none)");
|
||||
}
|
||||
else
|
||||
{
|
||||
OSD::AddMessage(
|
||||
fmt::format("Texture Hash {}: {:016x}", enabled ? "added" : "removed",
|
||||
static_cast<unsigned long long>(texture_hash)));
|
||||
}
|
||||
}
|
||||
|
||||
if (IsHotkey(HK_VR_SHADER_SAVE_OVERRIDE))
|
||||
{
|
||||
const std::string game_id = SConfig::GetInstance().GetGameID();
|
||||
if (game_id.empty())
|
||||
{
|
||||
OSD::AddMessage("Save Shader Override: no game is running");
|
||||
}
|
||||
else if (shader_hunter.SaveSelectedShaderOverride(game_id, shader_hotkey_handling))
|
||||
{
|
||||
Settings::Instance().NotifyShaderOverridesChanged();
|
||||
OSD::AddMessage(fmt::format("Saved {} shader override to {}.ini",
|
||||
HandlingTypeLabel(shader_hotkey_handling), game_id));
|
||||
}
|
||||
else
|
||||
{
|
||||
OSD::AddMessage("Save Shader Override: no shader selected");
|
||||
}
|
||||
}
|
||||
|
||||
const bool throttle_hotkey_active = IsHotkey(HK_TOGGLE_THROTTLE, true);
|
||||
const bool finder_forces_turbo =
|
||||
CullingCodeFinder::GetInstance().IsTurboRuntimeOverrideActive();
|
||||
const bool finder_forces_audio_mute =
|
||||
CullingCodeFinder::GetInstance().IsAudioMuteRuntimeOverrideActive();
|
||||
const bool wants_temp_audio_mute =
|
||||
finder_forces_audio_mute ||
|
||||
(throttle_hotkey_active && Config::Get(Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT));
|
||||
|
||||
Core::SetIsThrottlerTempDisabled(throttle_hotkey_active || finder_forces_turbo);
|
||||
|
||||
if (wants_temp_audio_mute && !Config::Get(Config::MAIN_AUDIO_MUTED))
|
||||
{
|
||||
Config::SetCurrent(Config::MAIN_AUDIO_MUTED, true);
|
||||
AudioCommon::UpdateSoundStream(system);
|
||||
}
|
||||
else if (!IsHotkey(HK_TOGGLE_THROTTLE, true) && Config::Get(Config::MAIN_AUDIO_MUTED) &&
|
||||
else if (!wants_temp_audio_mute && Config::Get(Config::MAIN_AUDIO_MUTED) &&
|
||||
Config::GetActiveLayerForConfig(Config::MAIN_AUDIO_MUTED) ==
|
||||
Config::LayerType::CurrentRun)
|
||||
{
|
||||
|
||||
@@ -211,11 +211,27 @@ static std::vector<std::string> StringListToStdVector(QStringList list)
|
||||
return result;
|
||||
}
|
||||
|
||||
static QString GetMainWindowTitle()
|
||||
{
|
||||
QString title = QString::fromStdString(Common::GetScmRevStr());
|
||||
|
||||
if (title.startsWith(QStringLiteral("Dolphin ")))
|
||||
title.replace(0, QStringLiteral("Dolphin").size(), QStringLiteral("Dolphin-OpenXR"));
|
||||
|
||||
if (title.endsWith(QStringLiteral("-dirty")))
|
||||
{
|
||||
title.chop(QStringLiteral("-dirty").size());
|
||||
title.append(QStringLiteral(" -dirty"));
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(Core::System& system, std::unique_ptr<BootParameters> boot_parameters,
|
||||
const std::string& movie_path)
|
||||
: QMainWindow(nullptr), m_system(system)
|
||||
{
|
||||
setWindowTitle(QString::fromStdString(Common::GetScmRevStr()));
|
||||
setWindowTitle(GetMainWindowTitle());
|
||||
setWindowIcon(Resources::GetAppIcon());
|
||||
setUnifiedTitleAndToolBarOnMac(true);
|
||||
setAcceptDrops(true);
|
||||
@@ -694,6 +710,7 @@ void MainWindow::ConnectToolBar()
|
||||
connect(m_tool_bar, &ToolBar::SettingsPressed, this, &MainWindow::ShowSettingsWindow);
|
||||
connect(m_tool_bar, &ToolBar::ControllersPressed, this, &MainWindow::ShowControllersWindow);
|
||||
connect(m_tool_bar, &ToolBar::GraphicsPressed, this, &MainWindow::ShowGraphicsWindow);
|
||||
connect(m_tool_bar, &ToolBar::VRPressed, this, &MainWindow::ShowVRWindow);
|
||||
|
||||
connect(m_tool_bar, &ToolBar::StepPressed, m_code_widget, &CodeWidget::Step);
|
||||
connect(m_tool_bar, &ToolBar::StepOverPressed, m_code_widget, &CodeWidget::StepOver);
|
||||
@@ -1277,7 +1294,7 @@ void MainWindow::HideRenderWidget(bool reinit, bool is_exit)
|
||||
m_rendering_to_main = false;
|
||||
m_stack->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
|
||||
disconnect(Host::GetInstance(), &Host::RequestTitle, this, &MainWindow::setWindowTitle);
|
||||
setWindowTitle(QString::fromStdString(Common::GetScmRevStr()));
|
||||
setWindowTitle(GetMainWindowTitle());
|
||||
}
|
||||
|
||||
// The following code works around a driver bug that would lead to Dolphin crashing when changing
|
||||
@@ -1404,6 +1421,12 @@ void MainWindow::ShowGraphicsWindow()
|
||||
m_settings_window->SelectPane(SettingsWindowPaneIndex::Graphics);
|
||||
}
|
||||
|
||||
void MainWindow::ShowVRWindow()
|
||||
{
|
||||
ShowSettingsWindow();
|
||||
m_settings_window->SelectPane(SettingsWindowPaneIndex::VR);
|
||||
}
|
||||
|
||||
void MainWindow::ShowNetPlaySetupDialog()
|
||||
{
|
||||
m_netplay_setup_dialog->show();
|
||||
@@ -2024,7 +2047,8 @@ void MainWindow::ShowTASInput()
|
||||
|
||||
for (int i = 0; i < num_wii_controllers; i++)
|
||||
{
|
||||
if (Config::Get(Config::GetInfoForWiimoteSource(i)) == WiimoteSource::Emulated &&
|
||||
const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(i));
|
||||
if ((source == WiimoteSource::Emulated || source == WiimoteSource::OpenXR) &&
|
||||
(!Core::IsRunning(m_system) || m_system.IsWii()))
|
||||
{
|
||||
m_wii_tas_input_windows[i]->show();
|
||||
|
||||
@@ -171,6 +171,7 @@ private:
|
||||
void ShowAudioWindow();
|
||||
void ShowControllersWindow();
|
||||
void ShowGraphicsWindow();
|
||||
void ShowVRWindow();
|
||||
void ShowFreeLookWindow();
|
||||
void ShowAboutDialog();
|
||||
void ShowHotkeyDialog();
|
||||
|
||||
@@ -479,6 +479,11 @@ void Settings::RefreshGameList()
|
||||
emit GameListRefreshRequested();
|
||||
}
|
||||
|
||||
void Settings::NotifyShaderOverridesChanged()
|
||||
{
|
||||
emit ShaderOverridesChanged();
|
||||
}
|
||||
|
||||
void Settings::NotifyRefreshGameListStarted()
|
||||
{
|
||||
emit GameListRefreshStarted();
|
||||
|
||||
@@ -104,6 +104,7 @@ public:
|
||||
QString GetDefaultGame() const;
|
||||
void SetDefaultGame(QString path);
|
||||
void RefreshGameList();
|
||||
void NotifyShaderOverridesChanged();
|
||||
void NotifyRefreshGameListStarted();
|
||||
void NotifyRefreshGameListComplete();
|
||||
void NotifyMetadataRefreshComplete();
|
||||
@@ -200,6 +201,7 @@ signals:
|
||||
void TitleDBReloadRequested();
|
||||
void MetadataRefreshRequested();
|
||||
void MetadataRefreshCompleted();
|
||||
void ShaderOverridesChanged();
|
||||
void AutoRefreshToggled(bool enabled);
|
||||
void CursorVisibilityChanged();
|
||||
void LockCursorChanged();
|
||||
|
||||
@@ -101,9 +101,15 @@ void OnScreenDisplayPane::CreateLayout()
|
||||
m_show_statistics = new ConfigBool(tr("Show Statistics"), Config::GFX_OVERLAY_STATS);
|
||||
m_show_proj_statistics =
|
||||
new ConfigBool(tr("Show Projection Statistics"), Config::GFX_OVERLAY_PROJ_STATS);
|
||||
m_show_shader_flags =
|
||||
new ConfigBool(tr("Show Shader Flags"), Config::GFX_OVERLAY_SHADER_FLAGS);
|
||||
m_show_shader_hunting =
|
||||
new ConfigBool(tr("Shader Hunting"), Config::GFX_OVERLAY_SHADER_HUNTING);
|
||||
|
||||
debug_layout->addWidget(m_show_statistics, 0, 0);
|
||||
debug_layout->addWidget(m_show_proj_statistics, 0, 1);
|
||||
debug_layout->addWidget(m_show_shader_flags, 1, 0);
|
||||
debug_layout->addWidget(m_show_shader_hunting, 1, 1);
|
||||
|
||||
// Stack GroupBoxes
|
||||
auto* main_layout = new QVBoxLayout;
|
||||
@@ -217,6 +223,14 @@ void OnScreenDisplayPane::AddDescriptions()
|
||||
static const char TR_SHOW_PROJ_STATS_DESCRIPTION[] =
|
||||
QT_TR_NOOP("Shows various projection statistics.<br><br><dolphin_emphasis>If unsure, "
|
||||
"leave this unchecked.</dolphin_emphasis>");
|
||||
static const char TR_SHOW_SHADER_FLAGS_DESCRIPTION[] =
|
||||
QT_TR_NOOP("Shows ShaderHunter condition flag names and their current active/inactive "
|
||||
"status in the top-left corner."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
static const char TR_SHOW_SHADER_HUNTING_DESCRIPTION[] =
|
||||
QT_TR_NOOP("Shows Shader Hunting status in the top-left corner, including the selected "
|
||||
"shader hash, draw-call filter state, and selected texture hashes."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
|
||||
m_enable_osd->SetDescription(tr(TR_ENABLE_OSD_DESCRIPTION));
|
||||
m_font_size->SetDescription(tr(TR_OSD_FONT_SIZE_DESCRIPTION));
|
||||
@@ -242,4 +256,6 @@ void OnScreenDisplayPane::AddDescriptions()
|
||||
|
||||
m_show_statistics->SetDescription(tr(TR_SHOW_STATS_DESCRIPTION));
|
||||
m_show_proj_statistics->SetDescription(tr(TR_SHOW_PROJ_STATS_DESCRIPTION));
|
||||
m_show_shader_flags->SetDescription(tr(TR_SHOW_SHADER_FLAGS_DESCRIPTION));
|
||||
m_show_shader_hunting->SetDescription(tr(TR_SHOW_SHADER_HUNTING_DESCRIPTION));
|
||||
}
|
||||
@@ -48,4 +48,6 @@ private:
|
||||
// Debug
|
||||
ConfigBool* m_show_statistics;
|
||||
ConfigBool* m_show_proj_statistics;
|
||||
ConfigBool* m_show_shader_flags;
|
||||
ConfigBool* m_show_shader_hunting;
|
||||
};
|
||||
@@ -0,0 +1,575 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Settings/VRPane.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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/ConfigChoice.h"
|
||||
#include "DolphinQt/Config/ConfigControls/ConfigFloatSlider.h"
|
||||
#include "DolphinQt/Config/ConfigControls/ConfigSlider.h"
|
||||
#include "DolphinQt/Debugger/CullingCodeFinderWidget.h"
|
||||
#include "DolphinQt/Debugger/ShaderHunterWidget.h"
|
||||
#include "DolphinQt/Settings.h"
|
||||
#include "VideoCommon/HideObjectEngine.h"
|
||||
#include "VideoCommon/VideoConfig.h"
|
||||
|
||||
VRPane::VRPane(QWidget* parent) : QWidget(parent)
|
||||
{
|
||||
auto* main_layout = new QVBoxLayout;
|
||||
auto* tabs = new QTabWidget(this);
|
||||
auto* general_tab = new QWidget(this);
|
||||
auto* general_layout = new QVBoxLayout;
|
||||
auto* hack_tab = new QWidget(this);
|
||||
auto* hack_layout = new QVBoxLayout;
|
||||
auto* tools_tab = new QWidget(this);
|
||||
auto* tools_tab_layout = new QVBoxLayout;
|
||||
|
||||
general_tab->setLayout(general_layout);
|
||||
hack_tab->setLayout(hack_layout);
|
||||
tools_tab->setLayout(tools_tab_layout);
|
||||
tabs->addTab(general_tab, tr("General"));
|
||||
tabs->addTab(hack_tab, tr("Hacks"));
|
||||
tabs->addTab(tools_tab, tr("Tools"));
|
||||
|
||||
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_auto_immediate_xfb =
|
||||
new ConfigBool(tr("Force Immediately Present XFB"),
|
||||
Config::GFX_VR_AUTO_IMMEDIATE_XFB);
|
||||
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);
|
||||
|
||||
m_ar_mode = new ConfigBool(tr("AR Mode (Passthrough)"), Config::GFX_VR_AR_MODE);
|
||||
openxr_layout->addWidget(m_ar_mode, 1, 0, 1, 3);
|
||||
|
||||
m_ar_background_alpha =
|
||||
new ConfigFloatSlider(Config::GFX_VR_AR_BACKGROUND_ALPHA_MIN,
|
||||
Config::GFX_VR_AR_BACKGROUND_ALPHA_MAX,
|
||||
Config::GFX_VR_AR_BACKGROUND_ALPHA,
|
||||
Config::GFX_VR_AR_BACKGROUND_ALPHA_STEP);
|
||||
m_ar_background_alpha_value = new QLabel();
|
||||
openxr_layout->addWidget(
|
||||
new ConfigFloatLabel(tr("AR Background Alpha:"), m_ar_background_alpha), 16, 0);
|
||||
openxr_layout->addWidget(m_ar_background_alpha, 16, 1);
|
||||
openxr_layout->addWidget(m_ar_background_alpha_value, 16, 2);
|
||||
m_ar_background_alpha_value->setText(
|
||||
QString::asprintf("%.2f", m_ar_background_alpha->GetValue()));
|
||||
connect(m_ar_background_alpha, &ConfigFloatSlider::valueChanged, this, [this] {
|
||||
m_ar_background_alpha_value->setText(
|
||||
QString::asprintf("%.2f", m_ar_background_alpha->GetValue()));
|
||||
});
|
||||
|
||||
openxr_layout->addWidget(new ConfigFloatLabel(tr("Units per Meter:"), m_units_per_meter), 2, 0);
|
||||
openxr_layout->addWidget(m_units_per_meter, 2, 1);
|
||||
openxr_layout->addWidget(m_units_per_meter_value, 2, 2);
|
||||
openxr_layout->addWidget(new ConfigFloatLabel(tr("Lean Back Angle (deg):"), m_lean_back_angle), 3,
|
||||
0);
|
||||
openxr_layout->addWidget(m_lean_back_angle, 3, 1);
|
||||
openxr_layout->addWidget(m_lean_back_angle_value, 3, 2);
|
||||
openxr_layout->addWidget(new ConfigFloatLabel(tr("Camera Forward (m):"), m_camera_forward), 4, 0);
|
||||
openxr_layout->addWidget(m_camera_forward, 4, 1);
|
||||
openxr_layout->addWidget(m_camera_forward_value, 4, 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, 5, 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), 6, 0);
|
||||
openxr_layout->addWidget(m_screen_distance, 6, 1);
|
||||
openxr_layout->addWidget(m_screen_distance_value, 6, 2);
|
||||
openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Size (m):"), m_screen_size), 7, 0);
|
||||
openxr_layout->addWidget(m_screen_size, 7, 1);
|
||||
openxr_layout->addWidget(m_screen_size_value, 7, 2);
|
||||
openxr_layout->addWidget(
|
||||
new ConfigFloatLabel(tr("Head Locked Curvature:"), m_head_locked_curvature), 8, 0);
|
||||
openxr_layout->addWidget(m_head_locked_curvature, 8, 1);
|
||||
openxr_layout->addWidget(m_head_locked_curvature_value, 8, 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_opcode_replay_mode = new ConfigChoiceMap<OpenXROpcodeReplayMode>(
|
||||
{{tr("Off"), OpenXROpcodeReplayMode::Off},
|
||||
{tr("60 to 90"), OpenXROpcodeReplayMode::Replay60To90},
|
||||
{tr("30 to 90"), OpenXROpcodeReplayMode::Replay30To90}},
|
||||
Config::GFX_VR_OPCODE_REPLAY);
|
||||
openxr_layout->addWidget(new QLabel(tr("Opcode Replay:")), 9, 0);
|
||||
openxr_layout->addWidget(m_opcode_replay_mode, 9, 1, 1, 2);
|
||||
|
||||
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);
|
||||
|
||||
// VR Gamma
|
||||
m_vr_gamma = new ConfigFloatSlider(Config::GFX_VR_GAMMA_MIN, Config::GFX_VR_GAMMA_MAX,
|
||||
Config::GFX_VR_GAMMA, Config::GFX_VR_GAMMA_STEP);
|
||||
m_vr_gamma_value = new QLabel();
|
||||
openxr_layout->addWidget(new ConfigFloatLabel(tr("VR Gamma:"), m_vr_gamma), 12, 0);
|
||||
openxr_layout->addWidget(m_vr_gamma, 12, 1);
|
||||
openxr_layout->addWidget(m_vr_gamma_value, 12, 2);
|
||||
|
||||
auto update_gamma_label = [this] {
|
||||
const float val = m_vr_gamma->GetValue();
|
||||
m_vr_gamma_value->setText(val <= 1.01f ? tr("Off") : QString::asprintf("%.1f", val));
|
||||
};
|
||||
update_gamma_label();
|
||||
connect(m_vr_gamma, &ConfigFloatSlider::valueChanged, this, update_gamma_label);
|
||||
|
||||
// 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()));
|
||||
});
|
||||
|
||||
general_layout->addWidget(openxr_group);
|
||||
|
||||
auto* hacks_group = new QGroupBox(tr("VR Hacks"));
|
||||
auto* hacks_group_layout = new QVBoxLayout;
|
||||
hacks_group->setLayout(hacks_group_layout);
|
||||
|
||||
m_dont_clear_screen =
|
||||
new ConfigBool(tr("Don't Clear Screen"), Config::GFX_VR_DONT_CLEAR_SCREEN);
|
||||
m_disable_cpu_cull =
|
||||
new ConfigBool(tr("Disable CPU Culling in VR"), Config::GFX_VR_DISABLE_CPU_CULL);
|
||||
m_remove_bars = new ConfigBool(tr("Remove Cinematic Bars"), Config::GFX_VR_REMOVE_BARS);
|
||||
m_lock_head_pose =
|
||||
new ConfigBool(tr("Lock Head Pose Per Frame"), Config::GFX_VR_LOCK_HEAD_POSE);
|
||||
|
||||
hacks_group_layout->addWidget(m_auto_immediate_xfb);
|
||||
hacks_group_layout->addWidget(m_lock_head_pose);
|
||||
hacks_group_layout->addWidget(m_dont_clear_screen);
|
||||
hacks_group_layout->addWidget(m_disable_cpu_cull);
|
||||
hacks_group_layout->addWidget(m_remove_bars);
|
||||
hack_layout->addWidget(hacks_group);
|
||||
|
||||
// Lock Head Pose is incompatible with Immediately Present XFB — when enabled,
|
||||
// force-disable both Auto-enable Immediate XFB (local) and the Graphics Hacks
|
||||
// Immediately Present XFB config key, and grey them out.
|
||||
const auto update_lock_head_pose = [this] {
|
||||
const bool locked = m_lock_head_pose->isChecked();
|
||||
m_auto_immediate_xfb->setDisabled(locked);
|
||||
if (locked)
|
||||
{
|
||||
Config::SetBaseOrCurrent(Config::GFX_VR_AUTO_IMMEDIATE_XFB, false);
|
||||
Config::SetBaseOrCurrent(Config::GFX_HACK_IMMEDIATE_XFB, false);
|
||||
}
|
||||
};
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
connect(m_lock_head_pose, &QCheckBox::checkStateChanged, this,
|
||||
[update_lock_head_pose](Qt::CheckState) { update_lock_head_pose(); });
|
||||
#else
|
||||
connect(m_lock_head_pose, &QCheckBox::stateChanged, this,
|
||||
[update_lock_head_pose](int) { update_lock_head_pose(); });
|
||||
#endif
|
||||
update_lock_head_pose(); // Apply initial state
|
||||
|
||||
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);
|
||||
|
||||
auto* culling_finder_btn = new QPushButton(tr("Culling Code Finder"));
|
||||
connect(culling_finder_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 culling codes."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_culling_finder_widget)
|
||||
{
|
||||
m_culling_finder_widget->show();
|
||||
m_culling_finder_widget->raise();
|
||||
m_culling_finder_widget->activateWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
m_culling_finder_widget = new CullingCodeFinderWidget(this);
|
||||
m_culling_finder_widget->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
m_culling_finder_widget->show();
|
||||
connect(m_culling_finder_widget, &QObject::destroyed, this,
|
||||
[this] { m_culling_finder_widget = nullptr; });
|
||||
});
|
||||
tools_layout->addWidget(culling_finder_btn, 2, 0);
|
||||
|
||||
m_load_custom_shaders =
|
||||
new ConfigBool(tr("Load Custom Shaders"), Config::GFX_VR_LOAD_CUSTOM_SHADERS);
|
||||
tools_layout->addWidget(m_load_custom_shaders, 3, 0);
|
||||
|
||||
m_enable_openxr_config_scene = new ConfigBool(
|
||||
tr("Enable OpenXR Config Scene For Controller Binding"),
|
||||
Config::GFX_VR_ENABLE_OPENXR_CONFIG_SCENE);
|
||||
tools_layout->addWidget(m_enable_openxr_config_scene, 4, 0);
|
||||
|
||||
m_ar_mode_debug =
|
||||
new ConfigBool(tr("Fake AR Mode (Debug)"), Config::GFX_VR_AR_MODE_DEBUG);
|
||||
tools_layout->addWidget(m_ar_mode_debug, 5, 0);
|
||||
|
||||
general_layout->addStretch();
|
||||
hack_layout->addStretch();
|
||||
tools_tab_layout->addWidget(tools_group);
|
||||
tools_tab_layout->addStretch();
|
||||
|
||||
main_layout->addWidget(tabs);
|
||||
|
||||
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."
|
||||
"<br><br>When enabled, Dolphin uses OpenXR instead of Stereoscopic 3D output modes."
|
||||
"<br><br>This setting cannot be changed while emulation is active."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
static constexpr char TR_AUTO_IMMEDIATE_XFB_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Forces Immediately Present XFB on while OpenXR is enabled."
|
||||
"<br><br>This improves head-tracking responsiveness and reduces latency in VR by presenting "
|
||||
"each XFB copy as soon as it is created."
|
||||
"<br><br>Turn this off if a game flickers with Immediately Present XFB, then use "
|
||||
"<b>Lock Head Pose Per Frame</b> instead to keep head-tracking coherent."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
|
||||
static constexpr char TR_UNITS_PER_METER_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Sets how many game world units correspond to one real-world meter."
|
||||
"<br><br>Higher values increase stereo scale and make the world appear smaller."
|
||||
"<br><br>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."
|
||||
"<br><br>Use this to compensate games where the default viewpoint feels tilted."
|
||||
"<br><br>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."
|
||||
"<br><br>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."
|
||||
"<br><br>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)."
|
||||
"<br><br>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."
|
||||
"<br><br>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."
|
||||
"<br><br>0 keeps the screen flat. Higher values increase curvature."
|
||||
"<br><br>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."
|
||||
"<br><br>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."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
static constexpr char TR_DISABLE_CPU_CULL_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Disables CPU-side primitive culling when OpenXR VR is active."
|
||||
"<br><br>This may fix missing geometry in some games at the cost of a small performance hit."
|
||||
"<br><br>This only affects Dolphin's CPU culling optimization. It does not override "
|
||||
"the game's own backface culling state.");
|
||||
static constexpr char TR_OPCODE_REPLAY_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Submits extra synthetic OpenXR frames between real emulation frames to better match a "
|
||||
"90 Hz headset cadence."
|
||||
"<br><br>60 to 90 replays one extra HMD-only frame every other real frame."
|
||||
"<br><br>30 to 90 replays two extra HMD-only frames after each real frame."
|
||||
"<br><br>This only affects OpenXR HMD output. The desktop mirror stays at the real game "
|
||||
"frame cadence."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this set to Off.</dolphin_emphasis>");
|
||||
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."
|
||||
"<br><br>This overrides the Advanced tab's VBI percentage during VR sessions."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
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."
|
||||
"<br><br>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."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
|
||||
static constexpr char TR_LAYER_OFFSET_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Controls the depth separation between consecutive 2D layers on the virtual screen."
|
||||
"<br><br>Higher values spread layers further apart (stronger Z-fighting fix but may look "
|
||||
"wrong at extreme values). Lower values keep layers closer together."
|
||||
"<br><br>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."
|
||||
"<br><br>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."
|
||||
"<br><br>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."
|
||||
"<br><br>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."
|
||||
"<br><br>Recommended: 400-640 to clear only full-screen effects, or 0 to disable."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this at 0 (off).</dolphin_emphasis>");
|
||||
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."
|
||||
"<br><br>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."
|
||||
"<br><br>Disable this if it causes visual artifacts in a specific game."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
|
||||
static constexpr char TR_LOCK_HEAD_POSE_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Snaps OpenXR head-tracking updates to game-frame boundaries (XFB copies) so every draw "
|
||||
"call within a single game frame uses one consistent head pose."
|
||||
"<br><br>Without this, when <b>Immediately Present XFB</b> is disabled, "
|
||||
"<code>LocateViews()</code> can mutate the head pose mid-frame on the video thread, "
|
||||
"causing different parts of the same frame (e.g. level geometry vs. dynamic objects) "
|
||||
"to be rendered with different poses. The visible symptom is objects appearing to "
|
||||
""stay in place" and jump to their correct positions a few frames later."
|
||||
"<br><br>This option captures a single head pose at the XFB-copy point in FIFO order, "
|
||||
"guaranteeing all draws of a frame are coherent. It has no effect when "
|
||||
"<b>Immediately Present XFB</b> is on (that path is already coherent)."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
|
||||
static constexpr char TR_VR_GAMMA_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Adjusts gamma correction for VR eye output."
|
||||
"<br><br>1.0 = no correction (default). 2.2 = standard sRGB gamma. "
|
||||
"Higher values darken the image further."
|
||||
"<br><br>Some headsets expect sRGB-encoded content and appear "
|
||||
"too bright without correction. Adjust this slider until the brightness matches "
|
||||
"the desktop mirror."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this at 1.0 (Off).</dolphin_emphasis>");
|
||||
static constexpr char TR_LOAD_CUSTOM_SHADERS_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Loads custom shader replacements from the Load/Shaders/<GameID>/ directory."
|
||||
"<br><br>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."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
static constexpr char TR_ENABLE_OPENXR_CONFIG_SCENE_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Starts a temporary OpenXR scene while configuring an <b>OpenXR Wii Remote</b> with no "
|
||||
"game running."
|
||||
"<br><br>This keeps OpenXR controller input active so the mapping window can detect button "
|
||||
"presses and motion input outside emulation."
|
||||
"<br><br>If the temporary session cannot start, Dolphin falls back to the desktop-only "
|
||||
"mapping window."
|
||||
"<br><br>Disable this if your runtime or headset has trouble with the temporary binding "
|
||||
"scene."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
|
||||
static constexpr char TR_AR_MODE_DEBUG_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Debug aid for AR Mode development on headsets that do <b>not</b> support passthrough."
|
||||
"<br><br>Clears the per-eye OpenXR swapchain to <b>solid magenta</b> instead of "
|
||||
"transparent black, so the regions that <i>would</i> be see-through in real AR Mode "
|
||||
"become clearly visible. The OpenXR composition state is left as <code>OPAQUE</code>, "
|
||||
"so this works on any headset and never triggers "
|
||||
"<code>XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED</code>."
|
||||
"<br><br>Use this to verify that the eye framebuffer clear path, layer flag wiring, "
|
||||
"and game-geometry-on-empty-background composition all behave correctly without "
|
||||
"needing a passthrough-capable runtime."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
static constexpr char TR_AR_MODE_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Submits the OpenXR projection layer with alpha-blended composition so that empty "
|
||||
"(transparent) pixels reveal the headset passthrough feed, making game geometry float "
|
||||
"over the real world."
|
||||
"<br><br>Requires a headset and runtime that support "
|
||||
"<code>XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND</code> (e.g. Quest 3, Vive XR Elite, "
|
||||
"Varjo, HoloLens). On opaque-only PCVR headsets (Index, Vive Pro) enabling this will "
|
||||
"cause <code>xrEndFrame</code> to fail; disable it again if the VR session stops "
|
||||
"submitting frames."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
|
||||
m_enable_openxr->SetDescription(tr(TR_ENABLE_OPENXR_DESCRIPTION));
|
||||
m_auto_immediate_xfb->SetDescription(tr(TR_AUTO_IMMEDIATE_XFB_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_opcode_replay_mode->SetDescription(tr(TR_OPCODE_REPLAY_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_lock_head_pose->SetDescription(tr(TR_LOCK_HEAD_POSE_DESCRIPTION));
|
||||
m_vr_gamma->SetDescription(tr(TR_VR_GAMMA_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));
|
||||
m_enable_openxr_config_scene->SetDescription(tr(TR_ENABLE_OPENXR_CONFIG_SCENE_DESCRIPTION));
|
||||
m_ar_mode->SetDescription(tr(TR_AR_MODE_DESCRIPTION));
|
||||
m_ar_mode_debug->SetDescription(tr(TR_AR_MODE_DEBUG_DESCRIPTION));
|
||||
}
|
||||
|
||||
void VRPane::OnEmulationStateChanged(Core::State state)
|
||||
{
|
||||
const bool running = state != Core::State::Uninitialized;
|
||||
m_enable_openxr->setEnabled(!running);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPointer>
|
||||
#include <QWidget>
|
||||
|
||||
class ConfigBool;
|
||||
class ConfigFloatSlider;
|
||||
class ConfigSlider;
|
||||
class QLabel;
|
||||
class CullingCodeFinderWidget;
|
||||
class ShaderHunterWidget;
|
||||
template <typename T>
|
||||
class ConfigChoiceMap;
|
||||
enum class OpenXROpcodeReplayMode : int;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
enum class State;
|
||||
}
|
||||
|
||||
class VRPane final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VRPane(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
void AddDescriptions();
|
||||
void OnEmulationStateChanged(Core::State state);
|
||||
|
||||
ConfigBool* m_enable_openxr = nullptr;
|
||||
ConfigBool* m_auto_immediate_xfb = nullptr;
|
||||
ConfigFloatSlider* m_units_per_meter = nullptr;
|
||||
QLabel* m_units_per_meter_value = nullptr;
|
||||
ConfigFloatSlider* m_lean_back_angle = nullptr;
|
||||
QLabel* m_lean_back_angle_value = nullptr;
|
||||
ConfigFloatSlider* m_camera_forward = nullptr;
|
||||
QLabel* m_camera_forward_value = nullptr;
|
||||
ConfigBool* m_virtual_screen = nullptr;
|
||||
ConfigFloatSlider* m_screen_distance = nullptr;
|
||||
QLabel* m_screen_distance_value = nullptr;
|
||||
ConfigFloatSlider* m_screen_size = nullptr;
|
||||
QLabel* m_screen_size_value = nullptr;
|
||||
ConfigFloatSlider* m_head_locked_curvature = nullptr;
|
||||
QLabel* m_head_locked_curvature_value = nullptr;
|
||||
ConfigBool* m_dont_clear_screen = nullptr;
|
||||
ConfigBool* m_disable_cpu_cull = nullptr;
|
||||
ConfigChoiceMap<OpenXROpcodeReplayMode>* m_opcode_replay_mode = nullptr;
|
||||
ConfigBool* m_auto_vbi_from_hmd = nullptr;
|
||||
ConfigSlider* m_clear_efb_slider = nullptr;
|
||||
QLabel* m_clear_efb_value = nullptr;
|
||||
ConfigBool* m_remove_bars = nullptr;
|
||||
ConfigBool* m_lock_head_pose = nullptr;
|
||||
ConfigBool* m_ar_mode = nullptr;
|
||||
ConfigFloatSlider* m_ar_background_alpha = nullptr;
|
||||
QLabel* m_ar_background_alpha_value = nullptr;
|
||||
ConfigFloatSlider* m_vr_gamma = nullptr;
|
||||
QLabel* m_vr_gamma_value = nullptr;
|
||||
ConfigBool* m_auto_layer_spread = nullptr;
|
||||
ConfigFloatSlider* m_layer_offset = nullptr;
|
||||
QLabel* m_layer_offset_value = nullptr;
|
||||
ConfigFloatSlider* m_element_depth = nullptr;
|
||||
QLabel* m_element_depth_value = nullptr;
|
||||
ConfigBool* m_load_custom_shaders = nullptr;
|
||||
ConfigBool* m_enable_openxr_config_scene = nullptr;
|
||||
ConfigBool* m_ar_mode_debug = nullptr;
|
||||
QPointer<CullingCodeFinderWidget> m_culling_finder_widget;
|
||||
QPointer<ShaderHunterWidget> m_shader_hunter_widget;
|
||||
};
|
||||
@@ -128,13 +128,14 @@ void ToolBar::MakeActions()
|
||||
|
||||
m_config_action = addAction(tr("Config"), this, &ToolBar::SettingsPressed);
|
||||
m_graphics_action = addAction(tr("Graphics"), this, &ToolBar::GraphicsPressed);
|
||||
m_vr_action = addAction(tr("VR"), this, &ToolBar::VRPressed);
|
||||
m_controllers_action = addAction(tr("Controllers"), this, &ToolBar::ControllersPressed);
|
||||
|
||||
// Ensure every button has about the same width
|
||||
std::vector<QWidget*> items;
|
||||
for (const auto& action :
|
||||
{m_open_action, m_pause_play_action, m_stop_action, m_stop_action, m_fullscreen_action,
|
||||
m_screenshot_action, m_config_action, m_graphics_action, m_controllers_action,
|
||||
m_screenshot_action, m_config_action, m_graphics_action, m_vr_action, m_controllers_action,
|
||||
m_step_action, m_step_over_action, m_step_out_action, m_skip_action, m_show_pc_action,
|
||||
m_set_pc_action})
|
||||
{
|
||||
@@ -193,4 +194,8 @@ void ToolBar::UpdateIcons()
|
||||
m_config_action->setIcon(Resources::GetThemeIcon("config"));
|
||||
m_controllers_action->setIcon(Resources::GetThemeIcon("classic"));
|
||||
m_graphics_action->setIcon(Resources::GetThemeIcon("graphics"));
|
||||
QIcon vr_icon = Resources::GetThemeIcon("vr");
|
||||
if (vr_icon.isNull())
|
||||
vr_icon = Resources::GetThemeIcon("graphics");
|
||||
m_vr_action->setIcon(vr_icon);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ signals:
|
||||
void SettingsPressed();
|
||||
void ControllersPressed();
|
||||
void GraphicsPressed();
|
||||
void VRPressed();
|
||||
|
||||
void StepPressed();
|
||||
void StepOverPressed();
|
||||
@@ -57,6 +58,7 @@ private:
|
||||
QAction* m_config_action;
|
||||
QAction* m_controllers_action;
|
||||
QAction* m_graphics_action;
|
||||
QAction* m_vr_action;
|
||||
|
||||
QAction* m_step_action;
|
||||
QAction* m_step_over_action;
|
||||
|
||||
@@ -108,6 +108,12 @@ if(WIN32)
|
||||
ControllerInterface/ForceFeedback/ForceFeedbackDevice.cpp
|
||||
ControllerInterface/ForceFeedback/ForceFeedbackDevice.h
|
||||
)
|
||||
if(ENABLE_VR)
|
||||
target_sources(inputcommon PRIVATE
|
||||
ControllerInterface/OpenXR/OpenXR.cpp
|
||||
ControllerInterface/OpenXR/OpenXR.h
|
||||
)
|
||||
endif()
|
||||
elseif(APPLE)
|
||||
target_sources(inputcommon PRIVATE
|
||||
ControllerInterface/Quartz/Quartz.h
|
||||
|
||||
@@ -55,9 +55,18 @@ Cursor::Cursor(std::string name_, std::string ui_name_)
|
||||
// i18n: The symbol/abbreviation for degrees (unit of angular measure).
|
||||
_trans("°"),
|
||||
// i18n: Refers to emulated wii remote movements.
|
||||
_trans("Total rotation about the pitch axis.")},
|
||||
_trans("Total rotation about the pitch axis.")},
|
||||
20, 0, 360);
|
||||
|
||||
AddSetting(&m_sensitivity_setting,
|
||||
{_trans("Pointer Sensitivity"),
|
||||
// i18n: The percent symbol.
|
||||
_trans("%"),
|
||||
_trans("Scales pointer movement. Lower values require larger controller movement for "
|
||||
"the same cursor distance.")},
|
||||
100, 10, 300);
|
||||
|
||||
AddSetting(&m_invert_x_setting, {_trans("Invert Left/Right")}, false);
|
||||
AddSetting(&m_relative_setting, {_trans("Relative Input")}, false);
|
||||
AddSetting(&m_autohide_setting, {_trans("Auto-Hide")}, false);
|
||||
}
|
||||
@@ -90,14 +99,33 @@ Cursor::StateData Cursor::GetState(const bool adjusted,
|
||||
const ControllerEmu::InputOverrideFunction& override_func)
|
||||
{
|
||||
StateData state = GetState(adjusted);
|
||||
const ControlState sensitivity_scale = m_sensitivity_setting.GetValue() / ControlState(100.0);
|
||||
|
||||
if (!override_func)
|
||||
{
|
||||
if (state.IsVisible())
|
||||
{
|
||||
state.x = std::clamp(state.x * sensitivity_scale, ControlState(-1.0), ControlState(1.0));
|
||||
state.y = std::clamp(state.y * sensitivity_scale, ControlState(-1.0), ControlState(1.0));
|
||||
if (m_invert_x_setting.GetValue())
|
||||
state.x = -state.x;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
if (const std::optional<ControlState> x_override = override_func(name, X_INPUT_OVERRIDE, state.x))
|
||||
state.x = *x_override;
|
||||
if (const std::optional<ControlState> y_override = override_func(name, Y_INPUT_OVERRIDE, state.y))
|
||||
state.y = *y_override;
|
||||
|
||||
if (state.IsVisible())
|
||||
{
|
||||
state.x = std::clamp(state.x * sensitivity_scale, ControlState(-1.0), ControlState(1.0));
|
||||
state.y = std::clamp(state.y * sensitivity_scale, ControlState(-1.0), ControlState(1.0));
|
||||
if (m_invert_x_setting.GetValue())
|
||||
state.x = -state.x;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ private:
|
||||
SettingValue<double> m_yaw_setting;
|
||||
SettingValue<double> m_pitch_setting;
|
||||
SettingValue<double> m_vertical_offset_setting;
|
||||
SettingValue<double> m_sensitivity_setting;
|
||||
SettingValue<bool> m_invert_x_setting;
|
||||
|
||||
SettingValue<bool> m_relative_setting;
|
||||
SettingValue<bool> m_autohide_setting;
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
#ifdef CIFACE_USE_WIN32
|
||||
#include "InputCommon/ControllerInterface/Win32/Win32.h"
|
||||
#ifdef ENABLE_VR
|
||||
#include "InputCommon/ControllerInterface/OpenXR/OpenXR.h"
|
||||
#endif
|
||||
#endif
|
||||
#ifdef CIFACE_USE_XLIB
|
||||
#include "InputCommon/ControllerInterface/Xlib/XInput2.h"
|
||||
@@ -60,6 +63,9 @@ void ControllerInterface::Initialize(const WindowSystemInfo& wsi)
|
||||
|
||||
#ifdef CIFACE_USE_WIN32
|
||||
m_input_backends.emplace_back(ciface::Win32::CreateInputBackend(this));
|
||||
#ifdef ENABLE_VR
|
||||
m_input_backends.emplace_back(ciface::OpenXR::CreateInputBackend(this));
|
||||
#endif
|
||||
#endif
|
||||
#ifdef CIFACE_USE_XLIB
|
||||
m_input_backends.emplace_back(ciface::XInput2::CreateInputBackend(this));
|
||||
|
||||
@@ -276,6 +276,7 @@ void CreateDirectories()
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_JITBLOCKS_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_GAMESETTINGS_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_GAMESETTINGSVR_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + USA_DIR DIR_SEP);
|
||||
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + EUR_DIR DIR_SEP);
|
||||
|
||||
@@ -34,3 +34,12 @@ if(MSVC)
|
||||
# Add precompiled header
|
||||
target_link_libraries(videod3d PRIVATE use_pch)
|
||||
endif()
|
||||
|
||||
if(ENABLE_VR)
|
||||
target_sources(videod3d PRIVATE
|
||||
D3DOpenXR.cpp
|
||||
D3DOpenXR.h
|
||||
)
|
||||
target_link_libraries(videod3d PRIVATE openxr_loader)
|
||||
target_include_directories(videod3d PRIVATE "${CMAKE_SOURCE_DIR}/Externals/OpenXR/include")
|
||||
endif()
|
||||
@@ -122,6 +122,16 @@ void Gfx::SetPipeline(const AbstractPipeline* pipeline)
|
||||
}
|
||||
}
|
||||
|
||||
void Gfx::SetForcePixelShader(const AbstractShader* shader)
|
||||
{
|
||||
if (shader)
|
||||
{
|
||||
D3D::stateman->SetPixelShader(
|
||||
static_cast<const DXShader*>(shader)->GetD3DPixelShader());
|
||||
m_current_pipeline = nullptr; // force re-set on next SetPipeline
|
||||
}
|
||||
}
|
||||
|
||||
void Gfx::SetScissorRect(const MathUtil::Rectangle<int>& rc)
|
||||
{
|
||||
// TODO: Move to stateman
|
||||
|
||||
@@ -48,6 +48,7 @@ public:
|
||||
std::vector<AbstractTexture*> additional_color_attachments) override;
|
||||
|
||||
void SetPipeline(const AbstractPipeline* pipeline) override;
|
||||
void SetForcePixelShader(const AbstractShader* shader) override;
|
||||
void SetFramebuffer(AbstractFramebuffer* framebuffer) override;
|
||||
void SetAndDiscardFramebuffer(AbstractFramebuffer* framebuffer) override;
|
||||
void SetAndClearFramebuffer(AbstractFramebuffer* framebuffer, const ClearColor& color_value = {},
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
#include "VideoBackends/D3D/D3DVertexManager.h"
|
||||
#include "VideoBackends/D3DCommon/D3DCommon.h"
|
||||
|
||||
#ifdef ENABLE_VR
|
||||
#include "VideoBackends/D3D/D3DOpenXR.h"
|
||||
#endif
|
||||
|
||||
#include "VideoCommon/AbstractGfx.h"
|
||||
#include "VideoCommon/FramebufferManager.h"
|
||||
#include "VideoCommon/ShaderCache.h"
|
||||
@@ -26,6 +30,10 @@
|
||||
#include "VideoCommon/VideoCommon.h"
|
||||
#include "VideoCommon/VideoConfig.h"
|
||||
|
||||
#ifdef ENABLE_VR
|
||||
#include "VideoCommon/VR/OpenXRManager.h"
|
||||
#endif
|
||||
|
||||
namespace DX11
|
||||
{
|
||||
std::string VideoBackend::GetConfigName() const
|
||||
@@ -147,6 +155,23 @@ bool VideoBackend::Initialize(const WindowSystemInfo& wsi)
|
||||
FillBackendInfo();
|
||||
UpdateActiveConfig();
|
||||
|
||||
#ifdef ENABLE_VR
|
||||
INFO_LOG_FMT(VIDEO, "VR: stereo_mode={} (OpenXR={})", static_cast<int>(g_ActiveConfig.stereo_mode),
|
||||
g_ActiveConfig.stereo_mode == StereoMode::OpenXR ? "YES" : "NO");
|
||||
if (g_ActiveConfig.stereo_mode == StereoMode::OpenXR)
|
||||
{
|
||||
auto openxr = std::make_unique<DX11::D3DOpenXR>();
|
||||
if (!openxr->Initialize())
|
||||
{
|
||||
WARN_LOG_FMT(VIDEO, "OpenXR initialization failed; continuing without VR.");
|
||||
}
|
||||
else
|
||||
{
|
||||
DX11::g_openxr_d3d = std::move(openxr);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
std::unique_ptr<SwapChain> swap_chain;
|
||||
if (wsi.render_surface && !(swap_chain = SwapChain::Create(wsi)))
|
||||
{
|
||||
@@ -167,6 +192,14 @@ bool VideoBackend::Initialize(const WindowSystemInfo& wsi)
|
||||
|
||||
void VideoBackend::Shutdown()
|
||||
{
|
||||
#ifdef ENABLE_VR
|
||||
// Shut down VR before the D3D device is destroyed.
|
||||
// g_openxr_d3d first (releases DXTexture/DXFramebuffer wrappers),
|
||||
// then g_openxr (calls xrDestroySwapchain/Session/Instance — releases runtime D3D11 refs).
|
||||
// Both must happen before D3D::Destroy() to avoid "Unreleased references" warnings.
|
||||
DX11::g_openxr_d3d.reset();
|
||||
VR::g_openxr.reset();
|
||||
#endif
|
||||
ShutdownShared();
|
||||
D3D::Destroy();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#ifdef ENABLE_VR
|
||||
|
||||
// Must define the D3D11 platform before any OpenXR includes.
|
||||
#define XR_USE_GRAPHICS_API_D3D11
|
||||
|
||||
#include "VideoBackends/D3D/D3DOpenXR.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
|
||||
#include "VideoBackends/D3D/D3DBase.h"
|
||||
#include "VideoBackends/D3D/DXTexture.h"
|
||||
#include "VideoCommon/TextureConfig.h"
|
||||
#include "VideoCommon/VideoConfig.h"
|
||||
#include "VideoCommon/VR/OpenXRD3D11Common.h"
|
||||
#include "VideoCommon/VR/OpenXRManager.h"
|
||||
|
||||
namespace DX11
|
||||
{
|
||||
std::unique_ptr<D3DOpenXR> g_openxr_d3d;
|
||||
|
||||
D3DOpenXR::D3DOpenXR() = default;
|
||||
|
||||
D3DOpenXR::~D3DOpenXR()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool D3DOpenXR::Initialize()
|
||||
{
|
||||
INFO_LOG_FMT(VIDEO, "OpenXR D3D11: Starting initialization...");
|
||||
ASSERT_MSG(VIDEO, !VR::g_openxr, "OpenXRManager already initialized.");
|
||||
|
||||
auto mgr = std::make_unique<VR::OpenXRManager>();
|
||||
|
||||
// The D3D11 graphics binding extension is mandatory.
|
||||
const std::vector<const char*> extensions = {XR_KHR_D3D11_ENABLE_EXTENSION_NAME};
|
||||
if (!mgr->CreateInstance(extensions))
|
||||
return false;
|
||||
|
||||
if (!mgr->InitializeSystem())
|
||||
return false;
|
||||
|
||||
if (!mgr->EnumerateViewConfigurations())
|
||||
return false;
|
||||
|
||||
// Publish the manager globally so CreateSessionD3D11 can access instance/system IDs.
|
||||
VR::g_openxr = std::move(mgr);
|
||||
|
||||
if (!CreateSessionD3D11())
|
||||
{
|
||||
VR::g_openxr.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VR::g_openxr->CreateReferenceSpace())
|
||||
{
|
||||
VR::g_openxr.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CreateSwapchains())
|
||||
{
|
||||
VR::g_openxr.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Register this object as the swapchain provider so Presenter can acquire eye images.
|
||||
VR::g_openxr->SetSwapchain(this);
|
||||
|
||||
INFO_LOG_FMT(VIDEO, "OpenXR D3D11: Initialization complete.");
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3DOpenXR::Shutdown()
|
||||
{
|
||||
// Clear swapchain pointer before destroying swapchains so no dangling use occurs.
|
||||
if (VR::g_openxr)
|
||||
VR::g_openxr->SetSwapchain(nullptr);
|
||||
|
||||
DestroySwapchains();
|
||||
VR::g_openxr.reset();
|
||||
if (D3D::context)
|
||||
D3D::context->Flush();
|
||||
INFO_LOG_FMT(VIDEO, "OpenXR D3D11: Shut down.");
|
||||
}
|
||||
|
||||
bool D3DOpenXR::CreateSessionD3D11()
|
||||
{
|
||||
ASSERT(D3D::device != nullptr);
|
||||
ASSERT(VR::g_openxr != nullptr);
|
||||
|
||||
XrGraphicsRequirementsD3D11KHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR};
|
||||
if (!VR::D3D11OpenXR::QueryGraphicsRequirements(VR::g_openxr->GetInstance(),
|
||||
VR::g_openxr->GetSystemId(), &requirements))
|
||||
return false;
|
||||
|
||||
INFO_LOG_FMT(VIDEO,
|
||||
"OpenXR: D3D11 requirements — adapter LUID {:#010x}{:08x}, "
|
||||
"min feature level {:#x}",
|
||||
requirements.adapterLuid.HighPart, requirements.adapterLuid.LowPart,
|
||||
static_cast<int>(requirements.minFeatureLevel));
|
||||
|
||||
XrSession session = XR_NULL_HANDLE;
|
||||
if (!VR::D3D11OpenXR::CreateSessionFromRequirements(VR::g_openxr->GetInstance(),
|
||||
VR::g_openxr->GetSystemId(), requirements,
|
||||
D3D::device.Get(), &session))
|
||||
return false;
|
||||
|
||||
VR::g_openxr->SetSession(session);
|
||||
INFO_LOG_FMT(VIDEO, "OpenXR D3D11: Session created successfully.");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool D3DOpenXR::CreateSwapchains()
|
||||
{
|
||||
ASSERT(VR::g_openxr != nullptr);
|
||||
|
||||
const auto& view_cfgs = VR::g_openxr->GetViewConfigViews();
|
||||
int64_t swapchain_format = 0;
|
||||
if (!VR::D3D11OpenXR::SelectSwapchainFormat(VR::g_openxr->GetSession(), &swapchain_format))
|
||||
return false;
|
||||
|
||||
for (uint32_t eye = 0; eye < 2; ++eye)
|
||||
{
|
||||
auto& sc = m_eye_swapchains[eye];
|
||||
sc.width = view_cfgs[eye].recommendedImageRectWidth;
|
||||
sc.height = view_cfgs[eye].recommendedImageRectHeight;
|
||||
|
||||
// Format must come from xrEnumerateSwapchainFormats.
|
||||
XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
info.arraySize = 1;
|
||||
info.format = swapchain_format;
|
||||
info.width = sc.width;
|
||||
info.height = sc.height;
|
||||
info.mipCount = 1;
|
||||
info.faceCount = 1;
|
||||
info.sampleCount = 1;
|
||||
// We render into this swapchain image as a color target, but never sample from it.
|
||||
// Some runtimes reject the extra SAMPLED usage for certain formats.
|
||||
info.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
|
||||
XrResult result = xrCreateSwapchain(VR::g_openxr->GetSession(), &info, &sc.swapchain);
|
||||
if (XR_FAILED(result))
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO, "OpenXR: xrCreateSwapchain failed for eye {} ({}).", eye,
|
||||
static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enumerate the D3D11 textures backing this swapchain.
|
||||
uint32_t image_count = 0;
|
||||
xrEnumerateSwapchainImages(sc.swapchain, 0, &image_count, nullptr);
|
||||
|
||||
std::vector<XrSwapchainImageD3D11KHR> images(image_count, {XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR});
|
||||
xrEnumerateSwapchainImages(sc.swapchain, image_count, &image_count,
|
||||
reinterpret_cast<XrSwapchainImageBaseHeader*>(images.data()));
|
||||
|
||||
sc.textures.resize(image_count);
|
||||
sc.framebuffers.resize(image_count);
|
||||
|
||||
for (uint32_t i = 0; i < image_count; ++i)
|
||||
{
|
||||
// Adopt the runtime-owned texture. DXTexture::CreateAdopted reads the D3D11 texture
|
||||
// descriptor to build the TextureConfig and AddRefs the underlying resource.
|
||||
sc.textures[i] = DXTexture::CreateAdopted(ComPtr<ID3D11Texture2D>(images[i].texture));
|
||||
if (!sc.textures[i])
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO, "OpenXR: DXTexture::CreateAdopted failed for eye {}, image {}.",
|
||||
eye, i);
|
||||
return false;
|
||||
}
|
||||
|
||||
// No depth attachment for now; depth will be added in Phase 3 when the render path
|
||||
// is fully integrated with the EFB.
|
||||
sc.framebuffers[i] = DXFramebuffer::Create(sc.textures[i].get(), nullptr, {});
|
||||
if (!sc.framebuffers[i])
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO, "OpenXR: DXFramebuffer::Create failed for eye {}, image {}.",
|
||||
eye, i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
INFO_LOG_FMT(VIDEO, "OpenXR: Eye {} swapchain ready: {}x{}, {} images.", eye, sc.width,
|
||||
sc.height, image_count);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3DOpenXR::DestroySwapchains()
|
||||
{
|
||||
for (uint32_t eye = 0; eye < 2; ++eye)
|
||||
{
|
||||
auto& sc = m_eye_swapchains[eye];
|
||||
|
||||
if (m_image_acquired[eye] && sc.swapchain != XR_NULL_HANDLE)
|
||||
{
|
||||
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
const XrResult release_result = xrReleaseSwapchainImage(sc.swapchain, &release_info);
|
||||
if (XR_FAILED(release_result))
|
||||
{
|
||||
WARN_LOG_FMT(VIDEO,
|
||||
"OpenXR: xrReleaseSwapchainImage during shutdown failed for eye {} ({}).",
|
||||
eye, static_cast<int>(release_result));
|
||||
}
|
||||
m_image_acquired[eye] = false;
|
||||
}
|
||||
|
||||
// Release Dolphin wrappers before destroying the swapchain so the
|
||||
// runtime's textures are only freed after our references are gone.
|
||||
sc.framebuffers.clear();
|
||||
sc.textures.clear();
|
||||
|
||||
if (sc.swapchain != XR_NULL_HANDLE)
|
||||
{
|
||||
const XrResult destroy_result = xrDestroySwapchain(sc.swapchain);
|
||||
if (XR_FAILED(destroy_result))
|
||||
{
|
||||
WARN_LOG_FMT(VIDEO, "OpenXR: xrDestroySwapchain failed for eye {} ({}).", eye,
|
||||
static_cast<int>(destroy_result));
|
||||
}
|
||||
sc.swapchain = XR_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AbstractFramebuffer* D3DOpenXR::AcquireEyeFramebuffer(uint32_t eye_index)
|
||||
{
|
||||
ASSERT(eye_index < 2);
|
||||
auto& sc = m_eye_swapchains[eye_index];
|
||||
|
||||
XrSwapchainImageAcquireInfo acquire_info{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
if (XR_FAILED(xrAcquireSwapchainImage(sc.swapchain, &acquire_info,
|
||||
&m_acquired_image_index[eye_index])))
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO, "OpenXR: xrAcquireSwapchainImage failed for eye {}.", eye_index);
|
||||
return nullptr;
|
||||
}
|
||||
m_image_acquired[eye_index] = true;
|
||||
|
||||
// Block until the acquired image is safe to write.
|
||||
XrSwapchainImageWaitInfo wait_info{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
|
||||
wait_info.timeout = XR_INFINITE_DURATION;
|
||||
if (XR_FAILED(xrWaitSwapchainImage(sc.swapchain, &wait_info)))
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO, "OpenXR: xrWaitSwapchainImage failed for eye {}.", eye_index);
|
||||
|
||||
// Ensure we don't leak an acquired image if waiting fails.
|
||||
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
const XrResult release_result = xrReleaseSwapchainImage(sc.swapchain, &release_info);
|
||||
if (XR_FAILED(release_result))
|
||||
{
|
||||
WARN_LOG_FMT(VIDEO,
|
||||
"OpenXR: xrReleaseSwapchainImage after wait failure failed for eye {} ({}).",
|
||||
eye_index, static_cast<int>(release_result));
|
||||
}
|
||||
m_image_acquired[eye_index] = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return sc.framebuffers[m_acquired_image_index[eye_index]].get();
|
||||
}
|
||||
|
||||
void D3DOpenXR::ReleaseEyeTexture(uint32_t eye_index)
|
||||
{
|
||||
ASSERT(eye_index < 2);
|
||||
if (!m_image_acquired[eye_index])
|
||||
return;
|
||||
|
||||
XrSwapchainImageReleaseInfo release_info{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
const XrResult result = xrReleaseSwapchainImage(m_eye_swapchains[eye_index].swapchain,
|
||||
&release_info);
|
||||
if (XR_FAILED(result))
|
||||
{
|
||||
WARN_LOG_FMT(VIDEO, "OpenXR: xrReleaseSwapchainImage failed for eye {} ({}).", eye_index,
|
||||
static_cast<int>(result));
|
||||
}
|
||||
m_image_acquired[eye_index] = false;
|
||||
}
|
||||
|
||||
bool D3DOpenXR::SubmitFrame()
|
||||
{
|
||||
ASSERT(VR::g_openxr != nullptr);
|
||||
|
||||
const auto& eye_views = VR::g_openxr->GetEyeViews();
|
||||
|
||||
for (uint32_t eye = 0; eye < 2; ++eye)
|
||||
{
|
||||
auto& pv = m_projection_views[eye];
|
||||
pv = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW};
|
||||
pv.pose = eye_views[eye].pose;
|
||||
pv.fov = eye_views[eye].fov;
|
||||
pv.subImage.swapchain = m_eye_swapchains[eye].swapchain;
|
||||
pv.subImage.imageArrayIndex = 0;
|
||||
pv.subImage.imageRect = {
|
||||
{0, 0},
|
||||
{static_cast<int32_t>(m_eye_swapchains[eye].width),
|
||||
static_cast<int32_t>(m_eye_swapchains[eye].height)}};
|
||||
}
|
||||
|
||||
m_projection_layer = {XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
m_projection_layer.space = VR::g_openxr->GetReferenceSpace();
|
||||
m_projection_layer.viewCount = 2;
|
||||
m_projection_layer.views = m_projection_views.data();
|
||||
|
||||
if (VR::g_openxr->GetActiveBlendMode() == XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND)
|
||||
{
|
||||
m_projection_layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT |
|
||||
XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT;
|
||||
}
|
||||
|
||||
const std::vector<XrCompositionLayerBaseHeader*> layers = {
|
||||
reinterpret_cast<XrCompositionLayerBaseHeader*>(&m_projection_layer)};
|
||||
|
||||
return VR::g_openxr->EndFrame(layers);
|
||||
}
|
||||
|
||||
} // namespace DX11
|
||||
|
||||
#endif // ENABLE_VR
|
||||