8 Commits
Author SHA1 Message Date
Christophe 339a7ed52d Readme 2026-04-12 02:34:41 +02:00
Christophe 220a99b7a3 Readme 2026-04-12 02:32:04 +02:00
iChris4 49febf2cbe WII Controller Config Screen Overlay for OpenXR & Quest 3 Controller Fix 2026-04-12 02:08:02 +02:00
iChris4 95b569b681 WIP CullingCodeFinder 2026-04-11 18:11:02 +02:00
iChris4 677aa34050 Added ARMode by Clodo76 2026-04-11 18:08:04 +02:00
iChris4 4a70fd2fbe VR Pane UI 2026-04-10 19:51:45 +02:00
iChris4 1c16f73855 VRPane Settings 2026-04-09 02:11:22 +02:00
iChris4 2ed53222b4 Update Readme 2026-04-07 22:32:00 +02:00
39 changed files with 3862 additions and 257 deletions

No files matched your search

+27
View File
@@ -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.
@@ -186,6 +186,7 @@ const Info<float> GFX_STEREO_DEPTH_PERCENTAGE{{System::GFX, "Stereoscopy", "Ster
// 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};
@@ -195,6 +196,8 @@ 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};
@@ -203,6 +206,10 @@ const Info<bool> GFX_VR_AUTO_LAYER_SPREAD{{System::GFX, "VR", "AutoLayerSpread"}
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
@@ -162,6 +162,7 @@ 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;
@@ -182,6 +183,7 @@ 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;
@@ -189,6 +191,13 @@ 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;
+3
View File
@@ -171,6 +171,9 @@ static void ApplyGameVRConfigOverrides(std::string_view game_id)
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
+51 -21
View File
@@ -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), {});
});
}
+9 -2
View File
@@ -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);
+4
View File
@@ -180,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
@@ -251,6 +253,8 @@ 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
@@ -42,27 +42,68 @@ QString GetProjectionLabel(const ShaderHunter::RuntimeElementSignature& sig)
.arg(sig.ortho_layer);
}
QString FormatSeedCandidateLabel(const ElementsGroupManager::SeedCandidate& candidate)
QString FormatSeedCandidateLabel(const ElementsGroupManager::SeedCandidate& candidate,
const ShaderHunter::RuntimeElementSignature& active_mask)
{
const auto& sig = candidate.signature;
return QObject::tr("%1 | VP %2,%3 %4x%5 | SC %6,%7 %8,%9 | Draw #%10 | Seen %11x")
.arg(GetProjectionLabel(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(candidate.representative_draw.draw_index + 1)
.arg(candidate.occurrence_count);
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)
QString FormatSeedSummary(const ElementsGroupManager::SeedCandidate& candidate,
const ShaderHunter::RuntimeElementSignature& active_mask)
{
return QObject::tr("%1\nRepresentative Draw: #%2\nOccurrences: %3")
.arg(FormatSeedCandidateLabel(candidate))
.arg(FormatSeedCandidateLabel(candidate, active_mask))
.arg(candidate.representative_draw.draw_index + 1)
.arg(candidate.occurrence_count);
}
@@ -396,7 +437,7 @@ void ElementsHuntingDialog::UpdateDisplay()
m_seed_list->clear();
for (size_t i = 0; i < candidates.size(); ++i)
{
auto* item = new QListWidgetItem(FormatSeedCandidateLabel(candidates[i]));
auto* item = new QListWidgetItem(FormatSeedCandidateLabel(candidates[i], status.seed_signature));
item->setData(Qt::UserRole, static_cast<int>(i));
m_seed_list->addItem(item);
}
@@ -405,7 +446,7 @@ void ElementsHuntingDialog::UpdateDisplay()
{
for (int i = 0; i < m_seed_list->count(); ++i)
m_seed_list->item(i)->setText(
FormatSeedCandidateLabel(candidates[static_cast<size_t>(i)]));
FormatSeedCandidateLabel(candidates[static_cast<size_t>(i)], status.seed_signature));
}
if (status.selected_seed_index >= 0 && status.selected_seed_index < m_seed_list->count())
@@ -470,7 +511,8 @@ void ElementsHuntingDialog::UpdateDisplay()
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)]) :
FormatSeedSummary(candidates[static_cast<size_t>(status.selected_seed_index)],
status.seed_signature) :
tr("selected"))
.arg(groups.isEmpty() ? tr("(none)") :
groups.join(QStringLiteral(", ")))
@@ -28,8 +28,22 @@ HacksWidget::HacksWidget(GraphicsPane* gfx_pane) : m_game_layer{gfx_pane->GetCon
connect(gfx_pane, &GraphicsPane::BackendChanged, this, &HacksWidget::OnBackendChanged);
const auto update_openxr_immediate_xfb = [this] {
if (Config::Get(Config::GFX_VR_ENABLE_OPENXR) && !Config::Get(Config::GFX_HACK_IMMEDIATE_XFB))
// 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();
};
@@ -50,6 +50,7 @@
#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"
@@ -94,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);
@@ -445,6 +449,7 @@ void MappingWindow::UpdateDeviceList()
void MappingWindow::SetMappingType(MappingWindow::Type type)
{
m_mapping_type = type;
MappingWidget* widget;
switch (type)
@@ -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;
@@ -139,4 +141,5 @@ private:
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
};
@@ -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;
};
+7 -1
View File
@@ -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()
+13 -4
View File
@@ -38,6 +38,7 @@
#include "DolphinQt/Settings.h"
#include "InputCommon/ControlReference/ControlReference.h"
#include "VideoCommon/CullingCodeFinder.h"
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "VideoCommon/OnScreenDisplay.h"
@@ -796,15 +797,23 @@ void HotkeyScheduler::Run()
}
}
Core::SetIsThrottlerTempDisabled(IsHotkey(HK_TOGGLE_THROTTLE, true));
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));
if (IsHotkey(HK_TOGGLE_THROTTLE, true) && !Config::Get(Config::MAIN_AUDIO_MUTED) &&
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)
{
+212 -51
View File
@@ -8,6 +8,7 @@
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QTabWidget>
#include <QVBoxLayout>
#include "Core/Config/GraphicsSettings.h"
@@ -20,6 +21,7 @@
#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"
@@ -28,12 +30,29 @@
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,
@@ -51,16 +70,37 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
m_camera_forward_value = new QLabel();
openxr_layout->addWidget(m_enable_openxr, 0, 0, 1, 3);
openxr_layout->addWidget(new ConfigFloatLabel(tr("Units per Meter:"), m_units_per_meter), 1, 0);
openxr_layout->addWidget(m_units_per_meter, 1, 1);
openxr_layout->addWidget(m_units_per_meter_value, 1, 2);
openxr_layout->addWidget(new ConfigFloatLabel(tr("Lean Back Angle (deg):"), m_lean_back_angle), 2,
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, 2, 1);
openxr_layout->addWidget(m_lean_back_angle_value, 2, 2);
openxr_layout->addWidget(new ConfigFloatLabel(tr("Camera Forward (m):"), m_camera_forward), 3, 0);
openxr_layout->addWidget(m_camera_forward, 3, 1);
openxr_layout->addWidget(m_camera_forward_value, 3, 2);
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] {
@@ -77,7 +117,7 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
// Virtual screen settings
m_virtual_screen = new ConfigBool(tr("Virtual Screen for 2D Content"), Config::GFX_VR_VIRTUAL_SCREEN);
openxr_layout->addWidget(m_virtual_screen, 4, 0, 1, 3);
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,
@@ -96,16 +136,16 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
Config::GFX_VR_HEAD_LOCKED_CURVATURE_STEP);
m_head_locked_curvature_value = new QLabel();
openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Distance (m):"), m_screen_distance), 5, 0);
openxr_layout->addWidget(m_screen_distance, 5, 1);
openxr_layout->addWidget(m_screen_distance_value, 5, 2);
openxr_layout->addWidget(new ConfigFloatLabel(tr("Screen Size (m):"), m_screen_size), 6, 0);
openxr_layout->addWidget(m_screen_size, 6, 1);
openxr_layout->addWidget(m_screen_size_value, 6, 2);
openxr_layout->addWidget(new ConfigFloatLabel(tr("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), 7, 0);
openxr_layout->addWidget(m_head_locked_curvature, 7, 1);
openxr_layout->addWidget(m_head_locked_curvature_value, 7, 2);
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] {
@@ -122,25 +162,17 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
QString::asprintf("%.2f", m_head_locked_curvature->GetValue()));
});
m_dont_clear_screen =
new ConfigBool(tr("Don't Clear Screen"), Config::GFX_VR_DONT_CLEAR_SCREEN);
openxr_layout->addWidget(m_dont_clear_screen, 8, 0, 1, 3);
m_disable_cpu_cull =
new ConfigBool(tr("Disable CPU Culling in VR"), Config::GFX_VR_DISABLE_CPU_CULL);
openxr_layout->addWidget(m_disable_cpu_cull, 9, 0, 1, 3);
m_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:")), 10, 0);
openxr_layout->addWidget(m_opcode_replay_mode, 10, 1, 1, 2);
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, 11, 0, 1, 3);
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,
@@ -148,9 +180,9 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
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),
12, 0);
openxr_layout->addWidget(m_clear_efb_slider, 12, 1);
openxr_layout->addWidget(m_clear_efb_value, 12, 2);
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();
@@ -159,17 +191,13 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
update_clear_efb_label();
connect(m_clear_efb_slider, &ConfigSlider::valueChanged, this, update_clear_efb_label);
// Remove Cinematic Bars
m_remove_bars = new ConfigBool(tr("Remove Cinematic Bars"), Config::GFX_VR_REMOVE_BARS);
openxr_layout->addWidget(m_remove_bars, 13, 0, 1, 3);
// 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), 14, 0);
openxr_layout->addWidget(m_vr_gamma, 14, 1);
openxr_layout->addWidget(m_vr_gamma_value, 14, 2);
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();
@@ -181,16 +209,16 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
// 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, 15, 0, 1, 3);
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), 16, 0);
openxr_layout->addWidget(m_layer_offset, 16, 1);
openxr_layout->addWidget(m_layer_offset_value, 16, 2);
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] {
@@ -202,16 +230,56 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
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), 17, 0);
openxr_layout->addWidget(m_element_depth, 17, 1);
openxr_layout->addWidget(m_element_depth_value, 17, 2);
openxr_layout->addWidget(new ConfigFloatLabel(tr("Element Depth:"), m_element_depth), 15, 0);
openxr_layout->addWidget(m_element_depth, 15, 1);
openxr_layout->addWidget(m_element_depth_value, 15, 2);
m_element_depth_value->setText(QString::asprintf("%.4f", m_element_depth->GetValue()));
connect(m_element_depth, &ConfigFloatSlider::valueChanged, this, [this] {
m_element_depth_value->setText(QString::asprintf("%.4f", m_element_depth->GetValue()));
});
main_layout->addWidget(openxr_group);
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;
@@ -269,13 +337,51 @@ VRPane::VRPane(QWidget* parent) : QWidget(parent)
});
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, 2, 0);
tools_layout->addWidget(m_load_custom_shaders, 3, 0);
main_layout->addWidget(tools_group);
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);
main_layout->addStretch();
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);
@@ -293,6 +399,13 @@ void VRPane::AddDescriptions()
"<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."
@@ -372,6 +485,18 @@ void VRPane::AddDescriptions()
"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 "
"&quot;stay in place&quot; 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. "
@@ -385,8 +510,40 @@ void VRPane::AddDescriptions()
"<br><br>Place edited shader files (dumped via Shader Hunter) in this folder to override "
"the game's original shaders. Files must be named &lt;hash&gt;-&lt;vs|ps|gs&gt;.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));
@@ -400,11 +557,15 @@ void VRPane::AddDescriptions()
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)
+9
View File
@@ -10,6 +10,7 @@ class ConfigBool;
class ConfigFloatSlider;
class ConfigSlider;
class QLabel;
class CullingCodeFinderWidget;
class ShaderHunterWidget;
template <typename T>
class ConfigChoiceMap;
@@ -31,6 +32,7 @@ private:
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;
@@ -51,6 +53,10 @@ private:
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;
@@ -59,5 +65,8 @@ private:
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;
};
+14 -103
View File
@@ -18,82 +18,14 @@
#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;
static const char* DXGIFormatToString(int64_t format)
{
switch (static_cast<DXGI_FORMAT>(format))
{
case DXGI_FORMAT_R8G8B8A8_UNORM:
return "DXGI_FORMAT_R8G8B8A8_UNORM";
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
return "DXGI_FORMAT_R8G8B8A8_UNORM_SRGB";
case DXGI_FORMAT_B8G8R8A8_UNORM:
return "DXGI_FORMAT_B8G8R8A8_UNORM";
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
return "DXGI_FORMAT_B8G8R8A8_UNORM_SRGB";
case DXGI_FORMAT_R16G16B16A16_FLOAT:
return "DXGI_FORMAT_R16G16B16A16_FLOAT";
default:
return "UNKNOWN_DXGI_FORMAT";
}
}
static bool SelectSwapchainFormat(XrSession session, int64_t* out_format)
{
uint32_t format_count = 0;
XrResult result = xrEnumerateSwapchainFormats(session, 0, &format_count, nullptr);
if (XR_FAILED(result) || format_count == 0)
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrEnumerateSwapchainFormats (count) failed ({}).",
static_cast<int>(result));
return false;
}
std::vector<int64_t> runtime_formats(format_count);
result = xrEnumerateSwapchainFormats(session, format_count, &format_count, runtime_formats.data());
if (XR_FAILED(result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrEnumerateSwapchainFormats failed ({}).",
static_cast<int>(result));
return false;
}
for (const int64_t format : runtime_formats)
{
INFO_LOG_FMT(VIDEO, "OpenXR: Runtime swapchain format {} ({})", static_cast<long long>(format),
DXGIFormatToString(format));
}
static constexpr std::array<DXGI_FORMAT, 5> preferred_formats = {
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM,
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
DXGI_FORMAT_R16G16B16A16_FLOAT};
for (const DXGI_FORMAT preferred : preferred_formats)
{
const int64_t wanted = static_cast<int64_t>(preferred);
if (std::find(runtime_formats.begin(), runtime_formats.end(), wanted) != runtime_formats.end())
{
*out_format = wanted;
INFO_LOG_FMT(VIDEO, "OpenXR: Selected swapchain format {} ({}).",
static_cast<long long>(*out_format), DXGIFormatToString(*out_format));
return true;
}
}
*out_format = runtime_formats.front();
WARN_LOG_FMT(VIDEO,
"OpenXR: No preferred D3D11 swapchain format found; falling back to runtime format "
"{} ({}).",
static_cast<long long>(*out_format), DXGIFormatToString(*out_format));
return true;
}
D3DOpenXR::D3DOpenXR() = default;
D3DOpenXR::~D3DOpenXR()
@@ -165,27 +97,10 @@ bool D3DOpenXR::CreateSessionD3D11()
ASSERT(D3D::device != nullptr);
ASSERT(VR::g_openxr != nullptr);
// --- Query D3D11 graphics requirements (mandatory before session creation) ---
PFN_xrGetD3D11GraphicsRequirementsKHR pfnGetD3D11Requirements = nullptr;
XrResult result = xrGetInstanceProcAddr(
VR::g_openxr->GetInstance(), "xrGetD3D11GraphicsRequirementsKHR",
reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetD3D11Requirements));
if (XR_FAILED(result) || pfnGetD3D11Requirements == nullptr)
{
ERROR_LOG_FMT(VIDEO, "OpenXR: Could not load xrGetD3D11GraphicsRequirementsKHR.");
return false;
}
XrGraphicsRequirementsD3D11KHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR};
result = pfnGetD3D11Requirements(VR::g_openxr->GetInstance(), VR::g_openxr->GetSystemId(),
&requirements);
if (XR_FAILED(result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrGetD3D11GraphicsRequirementsKHR failed ({}).",
static_cast<int>(result));
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}, "
@@ -193,21 +108,11 @@ bool D3DOpenXR::CreateSessionD3D11()
requirements.adapterLuid.HighPart, requirements.adapterLuid.LowPart,
static_cast<int>(requirements.minFeatureLevel));
// --- Create XrSession bound to the active D3D11 device ---
XrGraphicsBindingD3D11KHR d3d11_binding{XR_TYPE_GRAPHICS_BINDING_D3D11_KHR};
d3d11_binding.device = D3D::device.Get();
XrSessionCreateInfo session_info{XR_TYPE_SESSION_CREATE_INFO};
session_info.next = &d3d11_binding;
session_info.systemId = VR::g_openxr->GetSystemId();
XrSession session = XR_NULL_HANDLE;
result = xrCreateSession(VR::g_openxr->GetInstance(), &session_info, &session);
if (XR_FAILED(result))
{
ERROR_LOG_FMT(VIDEO, "OpenXR: xrCreateSession failed ({}).", static_cast<int>(result));
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.");
@@ -220,7 +125,7 @@ bool D3DOpenXR::CreateSwapchains()
const auto& view_cfgs = VR::g_openxr->GetViewConfigViews();
int64_t swapchain_format = 0;
if (!SelectSwapchainFormat(VR::g_openxr->GetSession(), &swapchain_format))
if (!VR::D3D11OpenXR::SelectSwapchainFormat(VR::g_openxr->GetSession(), &swapchain_format))
return false;
for (uint32_t eye = 0; eye < 2; ++eye)
@@ -407,6 +312,12 @@ bool D3DOpenXR::SubmitFrame()
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)};
@@ -24,6 +24,7 @@
#include "VideoBackends/Vulkan/VKTexture.h"
#include "VideoBackends/Vulkan/VulkanContext.h"
#include "VideoCommon/TextureConfig.h"
#include "VideoCommon/VideoConfig.h"
#include "VideoCommon/VR/OpenXRManager.h"
#ifdef _WIN32
@@ -705,6 +706,12 @@ bool VulkanOpenXR::SubmitFrame()
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)};
+19
View File
@@ -380,11 +380,30 @@ bool ClearScreen(FramebufferManager* frame_buffer_manager, const MathUtil::Recta
alpha_enable = false;
}
// VR AR mode: force alpha clears so the EFB background starts at alpha=0, allowing
// the OpenXR compositor to show the headset passthrough through cleared regions.
// Debug AR mode forces a clear to opaque magenta so we can see the same regions on
// any headset (composition stays OPAQUE; no passthrough required).
if (g_ActiveConfig.vr_ar_mode || g_ActiveConfig.vr_ar_mode_debug)
{
color_enable = true;
alpha_enable = true;
}
if (color_enable || alpha_enable || z_enable)
{
u32 color = (clear_color_ar << 16) | clear_color_gb;
u32 z = clear_z_value;
// VR AR debug: paint the would-be-transparent background opaque magenta so we can
// see exactly which pixels passthrough would reveal, on any headset.
if (g_ActiveConfig.vr_ar_mode_debug)
color = 0xFFFF00FF;
// VR AR mode: force the cleared alpha byte to 0 (transparent) regardless of the
// game's requested clear color, so the passthrough background shows through.
else if (g_ActiveConfig.vr_ar_mode)
color &= 0x00FFFFFF;
VideoCommon::OpenXROpcodeReplay::ApplyReplayClearState(frame_just_rendered, &color_enable,
&alpha_enable, &z_enable,
&pixel_format, &color, &z);
+20
View File
@@ -42,6 +42,10 @@
#include "VideoCommon/VideoEvents.h"
#include "VideoCommon/XFStateManager.h"
#ifdef ENABLE_VR
#include "VideoCommon/VR/OpenXRManager.h"
#endif
using namespace BPFunctions;
static constexpr Common::EnumMap<float, GammaCorrection::Invalid2_2> s_gammaLUT = {1.0f, 1.7f, 2.2f,
@@ -378,6 +382,22 @@ static void BPWritten(PixelShaderManager& pixel_shader_manager, XFStateManager&
auto& vi = system.GetVideoInterface();
vi.FakeVIUpdate(destAddr, srcRect.GetWidth(), destStride, height);
}
#ifdef ENABLE_VR
// Lock Head Pose Per Frame: refresh the OpenXR head pose at this exact point
// in the FIFO stream — between the just-finished game frame and the next one.
// Because this runs in FIFO order on the video thread, every draw within a
// single game frame is guaranteed to see one consistent head pose, eliminating
// the level-vs-objects desync that occurs when LocateViews() races with draws
// via VI-event AsyncRequests in Present().
if (g_ActiveConfig.stereo_mode == StereoMode::OpenXR &&
g_ActiveConfig.vr_lock_head_pose && VR::g_openxr &&
VR::g_openxr->IsSessionRunning() && VR::g_openxr->ShouldRender())
{
VR::g_openxr->LocateViews();
system.GetGeometryShaderManager().InvalidateVRHeadPose();
}
#endif
}
}
+6
View File
@@ -178,6 +178,8 @@ add_library(videocommon
ShaderGenCommon.h
HideObjectEngine.cpp
HideObjectEngine.h
CullingCodeFinder.cpp
CullingCodeFinder.h
ShaderHunter.cpp
ShaderHunter.h
Spirv.cpp
@@ -306,8 +308,12 @@ endif()
if(ENABLE_VR)
target_sources(videocommon PRIVATE
VR/OpenXRD3D11Common.cpp
VR/OpenXRD3D11Common.h
VR/OpenXRManager.cpp
VR/OpenXRManager.h
VR/OpenXRUtilitySession.cpp
VR/OpenXRUtilitySession.h
)
# Expose the OpenXR headers to all targets that depend on videocommon.
target_include_directories(videocommon PUBLIC "${CMAKE_SOURCE_DIR}/Externals/OpenXR/include")
File diff suppressed because it is too large. Load diff
+240
View File
@@ -0,0 +1,240 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string>
#include <string_view>
#include <vector>
#include "Common/CommonTypes.h"
namespace Core
{
class System;
}
class CullingCodeFinder
{
public:
struct FrameStats
{
int draw_calls = 0;
int triangles_drawn = 0;
int prims = 0;
int drawn_objects = 0;
};
enum class PatchTemplateKind
{
LiR3True = 0,
LiR3False,
OriR0True,
OriR0False,
Count
};
struct ScanConfig
{
int savestate_slot = 1;
int settle_frames = 5;
int min_draw_call_increase = 1;
int min_triangle_increase = 0;
int min_object_increase = 0;
int timeout_seconds = 3;
int candidate_limit = 0;
u32 start_address = 0;
u32 end_address = 0;
bool turbo_during_scan = true;
bool mute_audio_during_scan = true;
bool auto_capture_reference_state = true;
std::string symbol_filter;
std::string output_folder;
std::array<bool, static_cast<size_t>(PatchTemplateKind::Count)> enabled_templates = {
true, true, true, true};
};
struct Candidate
{
std::string symbol_name;
u32 address = 0;
};
struct CandidateSource
{
std::string symbol_name;
u32 address = 0;
bool is_function = true;
bool valid_entry = true;
bool valid_stub = true;
};
struct ScanResult
{
std::string symbol_name;
u32 address = 0;
PatchTemplateKind template_kind = PatchTemplateKind::LiR3True;
FrameStats baseline{};
FrameStats measured{};
int draw_call_delta = 0;
int triangle_delta = 0;
int prim_delta = 0;
int object_delta = 0;
bool caused_timeout = false;
std::string screenshot_path;
std::string ar_code_text;
};
enum class ScanState
{
Idle,
Scanning,
Paused,
Stopped,
Finished,
Error,
};
struct ManifestData
{
std::string game_id;
std::string map_path;
ScanConfig config;
FrameStats baseline{};
bool baseline_set = false;
std::vector<Candidate> candidates;
std::vector<ScanResult> results;
size_t next_candidate_index = 0;
size_t next_template_index = 0;
bool has_last_tested_position = false;
u32 last_tested_address = 0;
size_t last_tested_template_index = 0;
ScanState state = ScanState::Idle;
};
struct PatchTemplate
{
PatchTemplateKind kind = PatchTemplateKind::LiR3True;
const char* display_name = "";
u32 first_instruction = 0;
};
static CullingCodeFinder& GetInstance();
void OnFrameEnd();
FrameStats GetLatestStats() const;
FrameStats GetBaseline() const;
bool HasBaseline() const { return m_baseline_set; }
void SetBaseline();
bool StartScan(Core::System& system, ScanConfig config, std::string* error = nullptr);
bool RestoreSession(Core::System& system, std::string_view output_folder,
std::string* error = nullptr, std::string* restored_message = nullptr);
void PauseScan();
void ResumeScan();
void StopScan();
void ClearResults(std::string* error = nullptr);
void NotifySavestateLoaded();
bool GenerateSymbolMap(Core::System& system, std::string* error = nullptr);
ScanState GetState() const;
float GetProgress() const;
int GetResultCount() const;
int GetCandidateCount() const;
std::string GetStatusText() const;
std::string GetMapPath() const;
ScanConfig GetConfig() const;
std::vector<ScanResult> GetResults() const;
bool IsTurboRuntimeOverrideActive() const;
bool IsAudioMuteRuntimeOverrideActive() const;
bool AdvanceScan(Core::System& system);
static std::string GetPatchTemplateName(PatchTemplateKind kind);
static std::array<std::string, 2> BuildARCodeLines(u32 address, PatchTemplateKind kind);
static std::string BuildARCodeText(u32 address, PatchTemplateKind kind);
static std::string GetDefaultOutputFolder(std::string_view game_id);
static std::vector<Candidate> BuildCandidatesFromSymbolInfo(
std::vector<CandidateSource> sources, std::string_view symbol_filter, int candidate_limit,
u32 start_address, u32 end_address);
static bool SaveManifest(const std::string& manifest_path, const ManifestData& manifest,
std::string* error = nullptr);
static bool LoadManifest(const std::string& manifest_path, ManifestData* manifest,
std::string* error = nullptr);
static bool AdvancePositionAfterCrash(const ScanConfig& config, size_t candidate_count,
size_t* candidate_index, size_t* template_index);
private:
CullingCodeFinder() = default;
enum class ScanPhase
{
None,
RequestLoad,
WaitingForLoad,
ApplyPatch,
WaitingForSettle,
Finalize,
};
bool BuildCandidatesFromMap(Core::System& system, std::string* error);
bool TryResumeFromManifest(Core::System& system, const ScanConfig& config, std::string* error);
bool StartFreshScan(Core::System& system, const ScanConfig& config, std::string* error);
bool CaptureReferenceSavestate(Core::System& system, std::string* error);
bool WriteManifest(std::string* error = nullptr) const;
void UpdateStatusText();
void RestorePatchedInstructions(Core::System& system);
void ApplyRuntimeOverrides(Core::System& system);
void RestoreRuntimeOverrides(Core::System& system);
void AdvanceToNextTemplate();
void SortResults();
FrameStats AverageSamples() const;
bool ShouldKeepResult(const FrameStats& measured) const;
std::string BuildResultScreenshotPath(const Candidate& candidate, PatchTemplateKind kind) const;
void AppendResult(ScanResult result);
int GetEnabledTemplateCount() const;
size_t FindNextEnabledTemplateIndex(size_t start_index) const;
size_t ComputeCompletedTests() const;
mutable std::mutex m_stats_mutex;
FrameStats m_display_stats{};
std::atomic<int> m_frame_counter{0};
FrameStats m_baseline{};
bool m_baseline_set = false;
ScanState m_state = ScanState::Idle;
ScanPhase m_phase = ScanPhase::None;
ScanConfig m_scan_config{};
std::string m_game_id;
std::string m_map_path;
std::string m_manifest_path;
std::string m_status_text = "Idle";
std::vector<Candidate> m_candidates;
std::vector<ScanResult> m_results;
size_t m_candidate_index = 0;
size_t m_template_index = 0;
bool m_has_last_tested_position = false;
u32 m_last_tested_address = 0;
size_t m_last_tested_template_index = 0;
u32 m_saved_entry_instruction = 0;
u32 m_saved_next_instruction = 0;
u32 m_patched_address = 0;
PatchTemplateKind m_active_template = PatchTemplateKind::LiR3True;
bool m_instruction_patched = false;
bool m_load_completed = false;
bool m_runtime_overrides_active = false;
bool m_turbo_override_active = false;
bool m_audio_mute_override_active = false;
int m_settle_frame_start = 0;
int m_last_sampled_frame = 0;
std::vector<FrameStats> m_settle_samples;
std::chrono::steady_clock::time_point m_phase_started_at{};
};
+115 -29
View File
@@ -360,6 +360,12 @@ size_t ComputeStableSubMatchBaseKey(const ElementsGroupManager::StableSubMatchSi
return seed;
}
bool GroupMaskHasActiveGroups(bool projection, bool layer, bool viewport, bool scissor,
bool render_state)
{
return projection || layer || viewport || scissor || render_state;
}
void SaveStableSubMatchSignature(std::ostringstream& out,
const ElementsGroupManager::StableSubMatchSignature& signature,
const std::string& prefix)
@@ -943,6 +949,7 @@ ElementsGroupManager::HuntingOption ElementsGroupManager::GetHuntingOption() con
void ElementsGroupManager::ClearSeedSelectionLocked()
{
m_seed_signature = {};
m_seed_group_signature = {};
m_seed_draw.reset();
m_selected_seed_index = -1;
m_selected_match = 0;
@@ -955,6 +962,84 @@ void ElementsGroupManager::ClearSeedSelectionLocked()
m_display_highlighted_match_raw_draw_count = 0;
}
ElementsGroupManager::RuntimeElementSignature ElementsGroupManager::GetMaskedSeedSignatureLocked() const
{
RuntimeElementSignature masked = m_seed_signature;
masked.use_projection = m_group_mask.projection;
masked.use_layer = m_group_mask.layer;
masked.use_viewport = m_group_mask.viewport;
masked.use_scissor = m_group_mask.scissor;
masked.use_render_state = m_group_mask.render_state;
return masked;
}
ElementsGroupManager::RuntimeElementSignature
ElementsGroupManager::GetSeedGroupSignatureLocked(const RuntimeElementSignature& signature) const
{
if (!signature.valid)
return {};
if (!GroupMaskHasActiveGroups(m_group_mask.projection, m_group_mask.layer, m_group_mask.viewport,
m_group_mask.scissor, m_group_mask.render_state))
return signature;
RuntimeElementSignature grouped{};
grouped.valid = true;
if (m_group_mask.projection)
{
grouped.perspective = signature.perspective;
if (signature.perspective)
{
grouped.perspective_hfov_x100 = signature.perspective_hfov_x100;
grouped.perspective_vfov_x100 = signature.perspective_vfov_x100;
grouped.perspective_near_x1000 = signature.perspective_near_x1000;
grouped.perspective_far_x100 = signature.perspective_far_x100;
}
else
{
grouped.ortho_left_x100 = signature.ortho_left_x100;
grouped.ortho_right_x100 = signature.ortho_right_x100;
grouped.ortho_top_x100 = signature.ortho_top_x100;
grouped.ortho_bottom_x100 = signature.ortho_bottom_x100;
}
}
if (m_group_mask.layer)
{
grouped.perspective = signature.perspective;
grouped.ortho_layer = signature.ortho_layer;
}
if (m_group_mask.viewport)
{
grouped.viewport_x = signature.viewport_x;
grouped.viewport_y = signature.viewport_y;
grouped.viewport_width = signature.viewport_width;
grouped.viewport_height = signature.viewport_height;
}
if (m_group_mask.scissor)
{
grouped.scissor_left = signature.scissor_left;
grouped.scissor_top = signature.scissor_top;
grouped.scissor_right = signature.scissor_right;
grouped.scissor_bottom = signature.scissor_bottom;
}
if (m_group_mask.render_state)
{
grouped.alpha_test_hex = signature.alpha_test_hex;
grouped.ztest = signature.ztest;
grouped.zupdate = signature.zupdate;
grouped.zfunc = signature.zfunc;
grouped.blend_color_update = signature.blend_color_update;
grouped.blend_alpha_update = signature.blend_alpha_update;
}
return grouped;
}
void ElementsGroupManager::SelectSeedCandidateLocked(int index)
{
if (index < 0 || index >= static_cast<int>(m_display_seed_candidates.size()))
@@ -964,7 +1049,8 @@ void ElementsGroupManager::SelectSeedCandidateLocked(int index)
}
m_selected_seed_index = index;
m_seed_signature = m_display_seed_candidates[index].signature;
m_seed_signature = m_display_seed_candidates[index].representative_draw.signature;
m_seed_group_signature = m_display_seed_candidates[index].group_signature;
m_seed_draw = m_display_seed_candidates[index].representative_draw;
m_selected_match = 0;
m_selected_match_filters.clear();
@@ -982,31 +1068,20 @@ void ElementsGroupManager::SelectSeedCandidateLocked(int index)
void ElementsGroupManager::SelectSeedCandidate(int index)
{
std::lock_guard lock(m_mutex);
const bool use_projection = m_seed_signature.use_projection;
const bool use_layer = m_seed_signature.use_layer;
const bool use_viewport = m_seed_signature.use_viewport;
const bool use_scissor = m_seed_signature.use_scissor;
const bool use_render_state = m_seed_signature.use_render_state;
SelectSeedCandidateLocked(index);
if (m_seed_signature.valid)
{
m_seed_signature.use_projection = use_projection;
m_seed_signature.use_layer = use_layer;
m_seed_signature.use_viewport = use_viewport;
m_seed_signature.use_scissor = use_scissor;
m_seed_signature.use_render_state = use_render_state;
}
}
void ElementsGroupManager::SetSeedGroupMask(bool projection, bool layer, bool viewport,
bool scissor, bool render_state)
{
std::lock_guard lock(m_mutex);
m_seed_signature.use_projection = projection;
m_seed_signature.use_layer = layer;
m_seed_signature.use_viewport = viewport;
m_seed_signature.use_scissor = scissor;
m_seed_signature.use_render_state = render_state;
m_group_mask.projection = projection;
m_group_mask.layer = layer;
m_group_mask.viewport = viewport;
m_group_mask.scissor = scissor;
m_group_mask.render_state = render_state;
if (m_seed_signature.valid)
m_seed_group_signature = GetSeedGroupSignatureLocked(m_seed_signature);
m_selected_match = 0;
m_selected_match_filters.clear();
m_selected_match_filters_excluded = false;
@@ -1164,7 +1239,7 @@ ElementsGroupManager::GetSelectedMatchDisplayDraws() const
ElementsGroupManager::RuntimeElementSignature ElementsGroupManager::GetSeedSignature() const
{
std::lock_guard lock(m_mutex);
return m_seed_signature;
return GetMaskedSeedSignatureLocked();
}
ElementsGroupManager::Status ElementsGroupManager::GetStatus() const
@@ -1175,7 +1250,7 @@ ElementsGroupManager::Status ElementsGroupManager::GetStatus() const
status.hunt_enabled = m_hunt_enabled;
status.option = m_hunting_option;
status.seed_valid = m_seed_signature.valid;
status.seed_signature = m_seed_signature;
status.seed_signature = GetMaskedSeedSignatureLocked();
status.seed_draw = m_seed_draw;
status.selected_seed_index = m_selected_seed_index;
status.total_seed_candidates = static_cast<int>(m_display_seed_candidates.size());
@@ -1266,7 +1341,8 @@ ElementsGroupManager::PreviewAction ElementsGroupManager::RegisterDraw(const Dra
recorded.draw_index = static_cast<int>(m_collecting_draws.size());
m_collecting_draws.push_back(recorded);
if (HasActiveHuntLocked() && RuntimeElementMatcher::Matches(m_seed_signature, recorded.signature))
if (HasActiveHuntLocked() &&
RuntimeElementMatcher::Matches(GetMaskedSeedSignatureLocked(), recorded.signature))
{
m_collecting_matches.push_back(recorded);
m_collecting_match_total++;
@@ -1338,17 +1414,23 @@ void ElementsGroupManager::OnFrameEnd()
m_display_seed_candidates.reserve(m_display_draws.size());
for (const DrawRecord& draw : m_display_draws)
{
const RuntimeElementSignature group_signature = GetSeedGroupSignatureLocked(draw.signature);
auto it = std::find_if(m_display_seed_candidates.begin(), m_display_seed_candidates.end(),
[&draw](const SeedCandidate& candidate) {
return SignaturesEqual(candidate.signature, draw.signature);
[&group_signature](const SeedCandidate& candidate) {
return SignaturesEqual(candidate.group_signature, group_signature);
});
if (it == m_display_seed_candidates.end())
{
m_display_seed_candidates.push_back(
SeedCandidate{.signature = draw.signature, .representative_draw = draw, .occurrence_count = 1});
m_display_seed_candidates.push_back(SeedCandidate{.signature = draw.signature,
.group_signature = group_signature,
.representative_draw = draw,
.occurrence_count = 1});
}
else
{
it->signature = draw.signature;
it->group_signature = group_signature;
it->representative_draw = draw;
it->occurrence_count++;
}
}
@@ -1357,7 +1439,8 @@ void ElementsGroupManager::OnFrameEnd()
{
const auto it = std::find_if(m_display_seed_candidates.begin(), m_display_seed_candidates.end(),
[this](const SeedCandidate& candidate) {
return SignaturesEqual(candidate.signature, m_seed_signature);
return SignaturesEqual(candidate.group_signature,
m_seed_group_signature);
});
if (it == m_display_seed_candidates.end())
{
@@ -1366,6 +1449,8 @@ void ElementsGroupManager::OnFrameEnd()
else
{
m_selected_seed_index = static_cast<int>(std::distance(m_display_seed_candidates.begin(), it));
m_seed_signature = it->representative_draw.signature;
m_seed_group_signature = it->group_signature;
m_seed_draw = it->representative_draw;
}
}
@@ -1406,8 +1491,9 @@ void ElementsGroupManager::OnFrameEnd()
bool ElementsGroupManager::HasActiveHuntLocked() const
{
return m_popup_open_count > 0 && m_hunt_enabled &&
RuntimeElementMatcher::HasActiveGroups(m_seed_signature);
return m_popup_open_count > 0 && m_hunt_enabled && m_seed_signature.valid &&
GroupMaskHasActiveGroups(m_group_mask.projection, m_group_mask.layer, m_group_mask.viewport,
m_group_mask.scissor, m_group_mask.render_state);
}
bool ElementsGroupManager::MatchesSelectedMatchFilterLocked(const DrawRecord& draw) const
@@ -103,6 +103,7 @@ public:
struct SeedCandidate
{
RuntimeElementSignature signature;
RuntimeElementSignature group_signature;
DrawRecord representative_draw;
int occurrence_count = 0;
};
@@ -209,9 +210,20 @@ private:
StableSubMatchSignature signature;
};
struct GroupMask
{
bool projection = false;
bool layer = false;
bool viewport = false;
bool scissor = false;
bool render_state = false;
};
void ClearSeedSelectionLocked();
void SelectSeedCandidateLocked(int index);
bool HasActiveHuntLocked() const;
RuntimeElementSignature GetMaskedSeedSignatureLocked() const;
RuntimeElementSignature GetSeedGroupSignatureLocked(const RuntimeElementSignature& signature) const;
bool MatchesSelectedMatchFilterLocked(const DrawRecord& draw) const;
bool MatchesSelectedMatchFilterSignatureLocked(const SelectedSubgroupSignature& signature) const;
std::vector<CurrentMatchCandidate> ResolveSelectedMatchDisplayDrawsLocked() const;
@@ -231,7 +243,9 @@ private:
bool m_hunt_enabled = false;
HuntingOption m_hunting_option = HuntingOption::Skip;
RuntimeElementSignature m_seed_signature{};
RuntimeElementSignature m_seed_group_signature{};
std::optional<DrawRecord> m_seed_draw;
GroupMask m_group_mask;
int m_selected_seed_index = -1;
int m_selected_match = 0;
std::vector<SelectedSubgroupSignature> m_selected_match_filters;
@@ -131,25 +131,43 @@ void GeometryShaderManager::SetConstants(PrimitiveType prim)
g_ActiveConfig.vr_units_per_meter,
0.0001f);
// Per-eye projection rows — needed by BOTH perspective and ortho VR paths.
std::array<std::array<float, 4>, 4> eye_projection_rows{};
std::array<std::array<float, 4>, 2> eye_z_rows{};
VR::g_openxr->GetEyeProjectionRows(upm, eye_projection_rows, eye_z_rows);
// OpenXR stereo path bypasses the classic cproj path, so apply freelook here too.
if (perspective && g_freelook_camera.IsActive())
// When vr_lock_head_pose is ON, only re-fetch the head pose from OpenXR when
// we've been explicitly invalidated (at the XFB-copy frame boundary). This
// prevents mid-frame LocateViews() updates from desynchronising different draw
// calls within the same game frame. When OFF, refetch every call (legacy
// behavior — kept as an escape hatch).
const bool need_refresh = !g_ActiveConfig.vr_lock_head_pose || m_vr_pose_needs_refresh;
if (need_refresh)
{
const Common::Matrix44 freelook_view = g_freelook_camera.GetView();
ApplyRowTransform(&eye_projection_rows, freelook_view);
ApplyRowTransform(&eye_z_rows, freelook_view);
std::array<std::array<float, 4>, 4> eye_projection_rows{};
std::array<std::array<float, 4>, 2> eye_z_rows{};
VR::g_openxr->GetEyeProjectionRows(upm, eye_projection_rows, eye_z_rows);
// OpenXR stereo path bypasses the classic cproj path, so apply freelook here too.
if (perspective && g_freelook_camera.IsActive())
{
const Common::Matrix44 freelook_view = g_freelook_camera.GetView();
ApplyRowTransform(&eye_projection_rows, freelook_view);
ApplyRowTransform(&eye_z_rows, freelook_view);
}
m_cached_eye_projection = eye_projection_rows;
m_cached_eye_z_row = eye_z_rows;
// Unrotated per-eye projection rows for head-locked content.
std::array<std::array<float, 4>, 4> head_proj_rows{};
VR::g_openxr->GetRawEyeProjectionRows(upm, head_proj_rows);
m_cached_head_projection = head_proj_rows;
m_vr_pose_needs_refresh = false;
}
constants.eye_projection[0] = eye_projection_rows[0];
constants.eye_projection[1] = eye_projection_rows[1];
constants.eye_projection[2] = eye_projection_rows[2];
constants.eye_projection[3] = eye_projection_rows[3];
constants.eye_z_row[0] = eye_z_rows[0];
constants.eye_z_row[1] = eye_z_rows[1];
constants.eye_projection[0] = m_cached_eye_projection[0];
constants.eye_projection[1] = m_cached_eye_projection[1];
constants.eye_projection[2] = m_cached_eye_projection[2];
constants.eye_projection[3] = m_cached_eye_projection[3];
constants.eye_z_row[0] = m_cached_eye_z_row[0];
constants.eye_z_row[1] = m_cached_eye_z_row[1];
if (perspective && g_backend_info.api_type == APIType::Vulkan)
{
@@ -187,13 +205,11 @@ void GeometryShaderManager::SetConstants(PrimitiveType prim)
constants.stereoparams[1] = 1.0f;
}
// Unrotated per-eye projection rows for head-locked content.
std::array<std::array<float, 4>, 4> head_proj_rows{};
VR::g_openxr->GetRawEyeProjectionRows(upm, head_proj_rows);
constants.head_projection[0] = head_proj_rows[0];
constants.head_projection[1] = head_proj_rows[1];
constants.head_projection[2] = head_proj_rows[2];
constants.head_projection[3] = head_proj_rows[3];
// Unrotated per-eye projection rows for head-locked content (cached above).
constants.head_projection[0] = m_cached_head_projection[0];
constants.head_projection[1] = m_cached_head_projection[1];
constants.head_projection[2] = m_cached_head_projection[2];
constants.head_projection[3] = m_cached_head_projection[3];
constants.head_locked_params = {g_ActiveConfig.vr_head_locked_curvature, 0.0f, 0.0f,
0.0f};
constants.pixel_center_correction = vertex_shader_manager.constants.pixelcentercorrection;
@@ -330,6 +346,11 @@ void GeometryShaderManager::SetProjectionChanged()
m_projection_changed = true;
}
void GeometryShaderManager::InvalidateVRHeadPose()
{
m_vr_pose_needs_refresh = true;
}
void GeometryShaderManager::SetLinePtWidthChanged()
{
constants.lineptparams[2] = bpmem.lineptwidth.linesize / 6.f;
@@ -3,6 +3,7 @@
#pragma once
#include <array>
#include <cmath>
#include <limits>
@@ -26,6 +27,11 @@ public:
void SetLinePtWidthChanged();
void SetTexCoordChanged(u8 texmapid);
// VR head-pose cache: marks the cached eye projection as stale so the next
// SetConstants() call re-fetches it from OpenXR. Called from BPStructs at the
// XFB-copy boundary so a single game frame's draws all see one consistent pose.
void InvalidateVRHeadPose();
GeometryShaderConstants constants{};
bool dirty = false;
@@ -51,4 +57,11 @@ private:
bool m_projection_changed = false;
bool m_viewport_changed = false;
// Cached OpenXR head pose data — refreshed only at frame boundaries (XFB copy)
// when vr_lock_head_pose is enabled, otherwise refreshed every SetConstants call.
std::array<std::array<float, 4>, 4> m_cached_eye_projection{};
std::array<std::array<float, 4>, 2> m_cached_eye_z_row{};
std::array<std::array<float, 4>, 4> m_cached_head_projection{};
bool m_vr_pose_needs_refresh = true;
};
+11 -2
View File
@@ -962,10 +962,19 @@ void Presenter::BlitCurrentSourceToOpenXREyes(const AbstractTexture* source_text
const MathUtil::Rectangle<int> eye_rect{
0, 0, static_cast<int>(sc->GetEyeWidth()), static_cast<int>(sc->GetEyeHeight())};
if (is_replay_present)
{
g_gfx->SetFramebuffer(eye_fb);
}
else
g_gfx->SetAndClearFramebuffer(eye_fb, {0.f, 0.f, 0.f, 1.f});
m_post_processor->BlitFromTexture(eye_rect, source_rc, source_texture, static_cast<int>(eye));
{
// In AR debug mode the EFB has already been cleared to opaque magenta by
// BPFunctions::ClearScreen, so the regular blit path produces the desired
// game-over-magenta image with composition state staying OPAQUE.
const float clear_alpha =
g_ActiveConfig.vr_ar_mode ? g_ActiveConfig.vr_ar_background_alpha : 1.0f;
g_gfx->SetAndClearFramebuffer(eye_fb, {0.f, 0.f, 0.f, clear_alpha});
m_post_processor->BlitFromTexture(eye_rect, source_rc, source_texture, static_cast<int>(eye));
}
sc->ReleaseEyeTexture(eye);
}
+5
View File
@@ -8,6 +8,7 @@
#include "VideoCommon/BPMemory.h"
#include "VideoCommon/TextureConfig.h"
#include "VideoCommon/VideoConfig.h"
void RasterizationState::Generate(const BPMemory& bp, PrimitiveType primitive_type)
{
@@ -117,6 +118,10 @@ void BlendingState::Generate(const BPMemory& bp)
color_update = bp.blendmode.color_update && alpha_test_may_succeed;
alpha_update = bp.blendmode.alpha_update && target_has_alpha && alpha_test_may_succeed;
// VR AR mode: always allow alpha writes so the OpenXR compositor receives a usable
// alpha channel for passthrough composition.
if (g_ActiveConfig.vr_ar_mode)
alpha_update = true;
const bool dst_alpha = bp.dstalpha.enable && alpha_update;
use_dual_src = true;
@@ -48,6 +48,7 @@ TCShaderUid GetShaderUid(EFBCopyFormat dst_format, bool is_depth_copy, bool is_i
uid_data->copy_filter_can_overflow = TextureCacheBase::CopyFilterCanOverflow(filter_coefficients);
// If the gamma is needed, then include that too.
uid_data->apply_gamma = gamma_rcp != 1.0f;
uid_data->vr_ar_mode = g_ActiveConfig.vr_ar_mode ? 1 : 0;
return out;
}
@@ -243,7 +244,10 @@ ShaderCode GeneratePixelShader(APIType api_type, const UidData* uid_data)
break;
case EFBCopyFormat::XFB:
out.Write(" ocol0 = float4(float3(texcol_raw.rgb) / 255.0, 1.0);\n");
if (uid_data->vr_ar_mode)
out.Write(" ocol0 = float4(texcol_raw.rgba) / 255.0;\n");
else
out.Write(" ocol0 = float4(float3(texcol_raw.rgb) / 255.0, 1.0);\n");
break;
default:
@@ -28,6 +28,7 @@ struct UidData
u32 all_copy_filter_coefs_needed : 1;
u32 copy_filter_can_overflow : 1;
u32 apply_gamma : 1;
u32 vr_ar_mode : 1;
};
#pragma pack()
@@ -44,6 +44,7 @@
#include "VideoCommon/VideoCommon.h"
#include "VideoCommon/VideoConfig.h"
#include "VideoCommon/XFMemory.h"
#include "VideoCommon/CullingCodeFinder.h"
#include "VideoCommon/HideObjectEngine.h"
#include "VideoCommon/ShaderHunter.h"
#include "VideoCommon/XFStateManager.h"
@@ -1310,6 +1311,7 @@ void VertexManagerBase::OnEndFrame()
HideObjectEngine::Engine::GetInstance().LoadCodesIfNeeded(game_id);
}
hunter.OnFrameEnd();
CullingCodeFinder::GetInstance().OnFrameEnd();
ElementsGroupManager::GetInstance().OnFrameEnd();
auto& system = Core::System::GetInstance();
system.GetGeometryShaderManager().vr_ortho_draw_counter = 0;
+4
View File
@@ -207,6 +207,10 @@ void VideoConfig::Refresh()
vr_opcode_replay_mode = Config::Get(Config::GFX_VR_OPCODE_REPLAY);
vr_auto_layer_spread = Config::Get(Config::GFX_VR_AUTO_LAYER_SPREAD);
vr_remove_bars = Config::Get(Config::GFX_VR_REMOVE_BARS);
vr_lock_head_pose = Config::Get(Config::GFX_VR_LOCK_HEAD_POSE);
vr_ar_mode = Config::Get(Config::GFX_VR_AR_MODE);
vr_ar_mode_debug = Config::Get(Config::GFX_VR_AR_MODE_DEBUG);
vr_ar_background_alpha = std::clamp(Config::Get(Config::GFX_VR_AR_BACKGROUND_ALPHA), 0.0f, 1.0f);
vr_gamma = std::clamp(Config::Get(Config::GFX_VR_GAMMA),
Config::GFX_VR_GAMMA_MIN, Config::GFX_VR_GAMMA_MAX);
vr_layer_offset = std::clamp(Config::Get(Config::GFX_VR_LAYER_OFFSET),
+4
View File
@@ -337,6 +337,10 @@ struct VideoConfig final
OpenXROpcodeReplayMode vr_opcode_replay_mode = OpenXROpcodeReplayMode::Off;
bool vr_auto_layer_spread = true;
bool vr_remove_bars = true; // Expand scissor/viewport to remove cinematic letterbox bars
bool vr_lock_head_pose = true; // Snap head-pose updates to XFB-copy boundaries (FIFO order)
bool vr_ar_mode = false; // Submit alpha-blended projection layer for headset passthrough
bool vr_ar_mode_debug = false; // Fake AR effect: clear eye FB to solid magenta (no XR blend mode change)
float vr_ar_background_alpha = 0.0f; // Eye-FB clear alpha when AR mode active (0 = transparent passthrough)
float vr_gamma = 1.0f; // Gamma for VR eye output (1.0=off, 2.2=sRGB, adjustable per headset)
float vr_layer_offset = 0.002f;
float vr_element_depth = 0.001f;
@@ -1 +1,2 @@
add_dolphin_test(VertexLoaderTest VertexLoaderTest.cpp)
add_dolphin_test(CullingCodeFinderTest CullingCodeFinderTest.cpp)
@@ -0,0 +1,266 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <gtest/gtest.h>
#include <array>
#include <string>
#include <vector>
#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Core/State.h"
#include "VideoCommon/CullingCodeFinder.h"
TEST(CullingCodeFinder, BuildCandidatesFromSymbolInfoFiltersSortsAndLimits)
{
std::vector<CullingCodeFinder::CandidateSource> sources = {
{"SkipNotFunction", 0x80003000, false, true, true},
{"SkipInvalidEntry", 0x80003010, true, false, true},
{"SkipInvalidStub", 0x80003020, true, true, false},
{"SkipBeforeStart", 0x80003030, true, true, true},
{"RenderSky", 0x80003120, true, true, true},
{"renderAlpha", 0x80003080, true, true, true},
{"RenderBeta", 0x80003040, true, true, true},
};
const auto filtered =
CullingCodeFinder::BuildCandidatesFromSymbolInfo(sources, "render", 2, 0x80003040,
0x80003100);
ASSERT_EQ(filtered.size(), 2u);
EXPECT_EQ(filtered[0].symbol_name, "RenderBeta");
EXPECT_EQ(filtered[0].address, 0x80003040u);
EXPECT_EQ(filtered[1].symbol_name, "renderAlpha");
EXPECT_EQ(filtered[1].address, 0x80003080u);
}
TEST(CullingCodeFinder, BuildARCodeLinesMatchesKnownHydraPatterns)
{
const auto i_ninja =
CullingCodeFinder::BuildARCodeLines(0x80052FE8, CullingCodeFinder::PatchTemplateKind::OriR0True);
EXPECT_EQ(i_ninja[0], "04052FE8 60000001");
EXPECT_EQ(i_ninja[1], "04052FEC 4E800020");
const auto metroid =
CullingCodeFinder::BuildARCodeLines(0x80345C68, CullingCodeFinder::PatchTemplateKind::LiR3True);
EXPECT_EQ(metroid[0], "04345C68 38600001");
EXPECT_EQ(metroid[1], "04345C6C 4E800020");
EXPECT_EQ(CullingCodeFinder::BuildARCodeText(0x80345C68,
CullingCodeFinder::PatchTemplateKind::LiR3True),
"04345C68 38600001\n04345C6C 4E800020");
}
TEST(CullingCodeFinder, AdvancePositionAfterCrashSkipsToNextPendingTest)
{
CullingCodeFinder::ScanConfig config;
config.enabled_templates = {true, false, true, true};
size_t candidate_index = 0;
size_t template_index = 2;
ASSERT_TRUE(
CullingCodeFinder::AdvancePositionAfterCrash(config, 2, &candidate_index, &template_index));
EXPECT_EQ(candidate_index, 0u);
EXPECT_EQ(template_index, 3u);
candidate_index = 0;
template_index = 3;
ASSERT_TRUE(
CullingCodeFinder::AdvancePositionAfterCrash(config, 2, &candidate_index, &template_index));
EXPECT_EQ(candidate_index, 1u);
EXPECT_EQ(template_index, 0u);
candidate_index = 1;
template_index = 3;
EXPECT_FALSE(
CullingCodeFinder::AdvancePositionAfterCrash(config, 2, &candidate_index, &template_index));
}
TEST(CullingCodeFinder, ManifestRoundTripPreservesResumeState)
{
const std::string temp_dir = File::CreateTempDir();
ASSERT_FALSE(temp_dir.empty());
const std::string manifest_path = temp_dir + DIR_SEP "manifest.ini";
CullingCodeFinder::ManifestData manifest;
manifest.game_id = "GM8E01";
manifest.map_path = temp_dir + DIR_SEP "GM8E01.map";
manifest.state = CullingCodeFinder::ScanState::Paused;
manifest.config.savestate_slot = 2;
manifest.config.settle_frames = 7;
manifest.config.min_draw_call_increase = 3;
manifest.config.min_triangle_increase = 11;
manifest.config.min_object_increase = 2;
manifest.config.timeout_seconds = 9;
manifest.config.candidate_limit = 15;
manifest.config.start_address = 0x80005200;
manifest.config.end_address = 0x80005300;
manifest.config.turbo_during_scan = false;
manifest.config.mute_audio_during_scan = false;
manifest.config.auto_capture_reference_state = false;
manifest.config.symbol_filter = "Cull";
manifest.config.output_folder = temp_dir + DIR_SEP "shots";
manifest.config.enabled_templates = {true, false, true, false};
manifest.baseline = {10, 20, 30, 40};
manifest.baseline_set = true;
manifest.candidates = {{"CheckCull", 0x80001000}, {"RenderCull", 0x80002000}};
manifest.next_candidate_index = 1;
manifest.next_template_index = 2;
manifest.has_last_tested_position = true;
manifest.last_tested_address = 0x80002000;
manifest.last_tested_template_index = 2;
CullingCodeFinder::ScanResult result;
result.symbol_name = "RenderCull";
result.address = 0x80345C68;
result.template_kind = CullingCodeFinder::PatchTemplateKind::LiR3True;
result.baseline = {10, 20, 30, 40};
result.measured = {15, 35, 45, 50};
result.draw_call_delta = 5;
result.triangle_delta = 15;
result.prim_delta = 15;
result.object_delta = 10;
result.caused_timeout = false;
result.screenshot_path = temp_dir + DIR_SEP "shots" DIR_SEP "04345C68_li.png";
result.ar_code_text = "04345C68 38600001\n04345C6C 4E800020";
manifest.results.push_back(result);
std::string error;
ASSERT_TRUE(CullingCodeFinder::SaveManifest(manifest_path, manifest, &error)) << error;
CullingCodeFinder::ManifestData loaded;
ASSERT_TRUE(CullingCodeFinder::LoadManifest(manifest_path, &loaded, &error)) << error;
EXPECT_EQ(loaded.game_id, manifest.game_id);
EXPECT_EQ(loaded.map_path, manifest.map_path);
EXPECT_EQ(loaded.state, manifest.state);
EXPECT_EQ(loaded.config.savestate_slot, manifest.config.savestate_slot);
EXPECT_EQ(loaded.config.settle_frames, manifest.config.settle_frames);
EXPECT_EQ(loaded.config.min_draw_call_increase, manifest.config.min_draw_call_increase);
EXPECT_EQ(loaded.config.min_triangle_increase, manifest.config.min_triangle_increase);
EXPECT_EQ(loaded.config.min_object_increase, manifest.config.min_object_increase);
EXPECT_EQ(loaded.config.timeout_seconds, manifest.config.timeout_seconds);
EXPECT_EQ(loaded.config.candidate_limit, manifest.config.candidate_limit);
EXPECT_EQ(loaded.config.start_address, manifest.config.start_address);
EXPECT_EQ(loaded.config.end_address, manifest.config.end_address);
EXPECT_EQ(loaded.config.turbo_during_scan, manifest.config.turbo_during_scan);
EXPECT_EQ(loaded.config.mute_audio_during_scan, manifest.config.mute_audio_during_scan);
EXPECT_EQ(loaded.config.auto_capture_reference_state,
manifest.config.auto_capture_reference_state);
EXPECT_EQ(loaded.config.symbol_filter, manifest.config.symbol_filter);
EXPECT_EQ(loaded.config.output_folder, manifest.config.output_folder);
EXPECT_EQ(loaded.config.enabled_templates, manifest.config.enabled_templates);
EXPECT_EQ(loaded.baseline.draw_calls, manifest.baseline.draw_calls);
EXPECT_EQ(loaded.baseline.triangles_drawn, manifest.baseline.triangles_drawn);
EXPECT_EQ(loaded.baseline.prims, manifest.baseline.prims);
EXPECT_EQ(loaded.baseline.drawn_objects, manifest.baseline.drawn_objects);
EXPECT_TRUE(loaded.baseline_set);
EXPECT_EQ(loaded.next_candidate_index, 1u);
EXPECT_EQ(loaded.next_template_index, 2u);
EXPECT_TRUE(loaded.has_last_tested_position);
EXPECT_EQ(loaded.last_tested_address, 0x80002000u);
EXPECT_EQ(loaded.last_tested_template_index, 2u);
ASSERT_EQ(loaded.candidates.size(), 2u);
EXPECT_EQ(loaded.candidates[1].symbol_name, "RenderCull");
EXPECT_EQ(loaded.candidates[1].address, 0x80002000u);
ASSERT_EQ(loaded.results.size(), 1u);
EXPECT_EQ(loaded.results[0].symbol_name, result.symbol_name);
EXPECT_EQ(loaded.results[0].address, result.address);
EXPECT_EQ(loaded.results[0].template_kind, result.template_kind);
EXPECT_EQ(loaded.results[0].draw_call_delta, result.draw_call_delta);
EXPECT_EQ(loaded.results[0].triangle_delta, result.triangle_delta);
EXPECT_EQ(loaded.results[0].object_delta, result.object_delta);
EXPECT_FALSE(loaded.results[0].caused_timeout);
EXPECT_EQ(loaded.results[0].screenshot_path, result.screenshot_path);
EXPECT_EQ(loaded.results[0].ar_code_text, result.ar_code_text);
File::DeleteDirRecursively(temp_dir);
}
TEST(CullingCodeFinder, ManifestRoundTripPreservesLastTestedPositionWithoutResults)
{
const std::string temp_dir = File::CreateTempDir();
ASSERT_FALSE(temp_dir.empty());
const std::string manifest_path = temp_dir + DIR_SEP "manifest.ini";
CullingCodeFinder::ManifestData manifest;
manifest.game_id = "GNJEAF";
manifest.map_path = temp_dir + DIR_SEP "GNJEAF.map";
manifest.state = CullingCodeFinder::ScanState::Scanning;
manifest.config.output_folder = temp_dir;
manifest.config.enabled_templates = {true, true, true, true};
manifest.baseline = {23, 0, 0, 0};
manifest.baseline_set = true;
manifest.candidates = {{"zz_80052fe8_", 0x80052FE8}, {"zz_80053010_", 0x80053010}};
manifest.next_candidate_index = 0;
manifest.next_template_index = 2;
manifest.has_last_tested_position = true;
manifest.last_tested_address = 0x80052FE8;
manifest.last_tested_template_index = 2;
std::string error;
ASSERT_TRUE(CullingCodeFinder::SaveManifest(manifest_path, manifest, &error)) << error;
CullingCodeFinder::ManifestData loaded;
ASSERT_TRUE(CullingCodeFinder::LoadManifest(manifest_path, &loaded, &error)) << error;
EXPECT_TRUE(File::Exists(manifest_path));
EXPECT_TRUE(loaded.results.empty());
EXPECT_EQ(loaded.next_candidate_index, 0u);
EXPECT_EQ(loaded.next_template_index, 2u);
EXPECT_TRUE(loaded.has_last_tested_position);
EXPECT_EQ(loaded.last_tested_address, 0x80052FE8u);
EXPECT_EQ(loaded.last_tested_template_index, 2u);
ASSERT_EQ(loaded.candidates.size(), 2u);
EXPECT_EQ(loaded.candidates[0].address, 0x80052FE8u);
File::DeleteDirRecursively(temp_dir);
}
TEST(CullingCodeFinder, ManifestRoundTripPreservesStoppedState)
{
const std::string temp_dir = File::CreateTempDir();
ASSERT_FALSE(temp_dir.empty());
const std::string manifest_path = temp_dir + DIR_SEP "manifest.ini";
CullingCodeFinder::ManifestData manifest;
manifest.game_id = "GNJEAF";
manifest.state = CullingCodeFinder::ScanState::Stopped;
manifest.config.output_folder = temp_dir;
manifest.baseline_set = true;
manifest.candidates = {{"zz_80052fe8_", 0x80052FE8}};
manifest.next_candidate_index = 0;
manifest.next_template_index = 2;
manifest.has_last_tested_position = true;
manifest.last_tested_address = 0x80052FE8;
manifest.last_tested_template_index = 2;
std::string error;
ASSERT_TRUE(CullingCodeFinder::SaveManifest(manifest_path, manifest, &error)) << error;
CullingCodeFinder::ManifestData loaded;
ASSERT_TRUE(CullingCodeFinder::LoadManifest(manifest_path, &loaded, &error)) << error;
EXPECT_EQ(loaded.state, CullingCodeFinder::ScanState::Stopped);
EXPECT_TRUE(loaded.has_last_tested_position);
EXPECT_EQ(loaded.last_tested_address, 0x80052FE8u);
EXPECT_EQ(loaded.last_tested_template_index, 2u);
File::DeleteDirRecursively(temp_dir);
}
TEST(State, SaveOptionsDefaultsMatchExpectedCompressionBehavior)
{
State::SaveOptions default_options;
EXPECT_TRUE(default_options.compress);
EXPECT_FALSE(default_options.wait_for_completion);
State::SaveOptions capture_options;
capture_options.compress = false;
capture_options.wait_for_completion = true;
EXPECT_FALSE(capture_options.compress);
EXPECT_TRUE(capture_options.wait_for_completion);
}