mirror of
https://github.com/thejerf/baijiSteamVR.git
synced 2026-10-18 00:02:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
339a7ed52d | ||
|
|
220a99b7a3 | ||
|
|
49febf2cbe | ||
|
|
95b569b681 | ||
|
|
677aa34050 | ||
|
|
4a70fd2fbe | ||
|
|
1c16f73855 | ||
|
|
2ed53222b4 |
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <locale>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
@@ -87,6 +88,7 @@ struct CompressAndDumpStateArgs
|
||||
{
|
||||
Common::UniqueBuffer<u8> buffer;
|
||||
std::string filename;
|
||||
CompressionType compression_type = CompressionType::LZ4;
|
||||
std::shared_lock<decltype(s_state_saves_in_progress)> task_lock;
|
||||
};
|
||||
|
||||
@@ -121,8 +123,6 @@ static const std::map<u32, std::pair<std::string, std::string>> s_old_versions =
|
||||
{38, {"4.0-4963", "4.0-5267"}}, {39, {"4.0-5279", "4.0-5525"}}, {40, {"4.0-5531", "4.0-5809"}},
|
||||
{41, {"4.0-5811", "4.0-5923"}}, {42, {"4.0-5925", "4.0-5946"}}};
|
||||
|
||||
static constexpr bool s_use_compression = true;
|
||||
|
||||
// Acquired for tasks that will write state save data to the filesystem.
|
||||
// This allows for later waiting on completion of said tasks when necessary.
|
||||
// We want to maintain a proper order of async operations, e.g. Save, Save, GetInfoString.
|
||||
@@ -353,19 +353,25 @@ static void CompressBufferToFile(std::span<const u8> raw_buffer, File::IOFile& f
|
||||
}
|
||||
}
|
||||
|
||||
static void CreateExtendedHeader(StateExtendedHeader& extended_header, size_t uncompressed_size)
|
||||
static CompressionType GetCompressionType(const SaveOptions& options)
|
||||
{
|
||||
return options.compress ? CompressionType::LZ4 : CompressionType::Uncompressed;
|
||||
}
|
||||
|
||||
static void CreateExtendedHeader(StateExtendedHeader& extended_header, size_t uncompressed_size,
|
||||
CompressionType compression_type)
|
||||
{
|
||||
StateExtendedBaseHeader& base_header = extended_header.base_header;
|
||||
base_header.header_version = EXTENDED_HEADER_VERSION;
|
||||
base_header.compression_type =
|
||||
s_use_compression ? CompressionType::LZ4 : CompressionType::Uncompressed;
|
||||
base_header.compression_type = compression_type;
|
||||
base_header.payload_offset = COMPRESSED_DATA_OFFSET;
|
||||
base_header.uncompressed_size = uncompressed_size;
|
||||
|
||||
// If more fields are added to StateExtendedHeader, set them here.
|
||||
}
|
||||
|
||||
static void WriteHeadersToFile(size_t uncompressed_size, File::IOFile& f)
|
||||
static void WriteHeadersToFile(size_t uncompressed_size, CompressionType compression_type,
|
||||
File::IOFile& f)
|
||||
{
|
||||
StateHeader header{};
|
||||
SConfig::GetInstance().GetGameID().copy(header.legacy_header.game_id,
|
||||
@@ -377,7 +383,7 @@ static void WriteHeadersToFile(size_t uncompressed_size, File::IOFile& f)
|
||||
header.version_header.version_string_length = static_cast<u32>(header.version_string.length());
|
||||
|
||||
StateExtendedHeader extended_header{};
|
||||
CreateExtendedHeader(extended_header, uncompressed_size);
|
||||
CreateExtendedHeader(extended_header, uncompressed_size, compression_type);
|
||||
|
||||
f.WriteArray(&header.legacy_header, 1);
|
||||
f.WriteArray(&header.version_header, 1);
|
||||
@@ -411,9 +417,9 @@ static void CompressAndDumpState(Core::System& system, const CompressAndDumpStat
|
||||
return;
|
||||
}
|
||||
|
||||
WriteHeadersToFile(buffer.size(), f);
|
||||
WriteHeadersToFile(buffer.size(), save_args.compression_type, f);
|
||||
|
||||
if (s_use_compression)
|
||||
if (save_args.compression_type == CompressionType::LZ4)
|
||||
CompressBufferToFile(buffer, f);
|
||||
else
|
||||
f.WriteBytes(buffer.data(), buffer.size());
|
||||
@@ -467,7 +473,7 @@ static void CompressAndDumpState(Core::System& system, const CompressAndDumpStat
|
||||
}
|
||||
}
|
||||
|
||||
static void SaveAsFromCore(Core::System& system, std::string filename)
|
||||
static void SaveAsFromCore(Core::System& system, std::string filename, SaveOptions options)
|
||||
{
|
||||
// Try with a buffer a bit larger than the previous state.
|
||||
// This will often avoid the "Measure" step.
|
||||
@@ -482,10 +488,13 @@ static void SaveAsFromCore(Core::System& system, std::string filename)
|
||||
CompressAndDumpStateArgs dump_args{
|
||||
.buffer = std::move(buffer),
|
||||
.filename = std::move(filename),
|
||||
.compression_type = GetCompressionType(options),
|
||||
.task_lock = GetStateSaveTaskLock(),
|
||||
};
|
||||
Core::DisplayMessage("Saving State...", 1000);
|
||||
s_compress_and_dump_thread.EmplaceItem(std::move(dump_args));
|
||||
if (options.wait_for_completion)
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -493,12 +502,28 @@ static void SaveAsFromCore(Core::System& system, std::string filename)
|
||||
}
|
||||
}
|
||||
|
||||
void SaveAs(Core::System& system, std::string filename)
|
||||
void SaveAs(Core::System& system, std::string filename, SaveOptions options)
|
||||
{
|
||||
Core::RunOnCPUThread(
|
||||
system, [&system, filename = std::move(filename), lock = GetStateSaveTaskLock()]() mutable {
|
||||
SaveAsFromCore(system, std::move(filename));
|
||||
});
|
||||
if (options.wait_for_completion)
|
||||
{
|
||||
std::promise<void> completion;
|
||||
auto future = completion.get_future();
|
||||
Core::RunOnCPUThread(system,
|
||||
[&system, filename = std::move(filename), options,
|
||||
lock = GetStateSaveTaskLock(),
|
||||
completion = std::move(completion)]() mutable {
|
||||
SaveAsFromCore(system, std::move(filename), options);
|
||||
completion.set_value();
|
||||
});
|
||||
future.wait();
|
||||
return;
|
||||
}
|
||||
|
||||
Core::RunOnCPUThread(system,
|
||||
[&system, filename = std::move(filename), options,
|
||||
lock = GetStateSaveTaskLock()]() mutable {
|
||||
SaveAsFromCore(system, std::move(filename), options);
|
||||
});
|
||||
}
|
||||
|
||||
static bool GetVersionFromLZO(StateHeader& header, File::IOFile& f)
|
||||
@@ -807,7 +832,7 @@ static void LoadFileStateData(const std::string& filename, Common::UniqueBuffer<
|
||||
static void LoadAsFromCore(Core::System& system, std::string filename)
|
||||
{
|
||||
// Ensure all data has reached the filesystem before trying to use it.
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
WaitForPendingSaves();
|
||||
|
||||
// Save temp buffer for undo load state
|
||||
auto& movie = system.GetMovie();
|
||||
@@ -899,9 +924,9 @@ void Shutdown()
|
||||
s_flush_unsaved_data_hook.reset();
|
||||
}
|
||||
|
||||
void Save(Core::System& system, int slot)
|
||||
void Save(Core::System& system, int slot, SaveOptions options)
|
||||
{
|
||||
SaveAs(system, MakeStateFilename(slot));
|
||||
SaveAs(system, MakeStateFilename(slot), options);
|
||||
}
|
||||
|
||||
void Load(Core::System& system, int slot)
|
||||
@@ -909,6 +934,11 @@ void Load(Core::System& system, int slot)
|
||||
LoadAs(system, MakeStateFilename(slot));
|
||||
}
|
||||
|
||||
void WaitForPendingSaves()
|
||||
{
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
}
|
||||
|
||||
void LoadLastSaved(Core::System& system, int i)
|
||||
{
|
||||
if (!CheckIfStateLoadIsAllowed(system))
|
||||
@@ -916,7 +946,7 @@ void LoadLastSaved(Core::System& system, int i)
|
||||
|
||||
Core::RunOnCPUThread(system, [&system, i] {
|
||||
// Data must reach the filesystem for up to date "UsedSlots".
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
WaitForPendingSaves();
|
||||
|
||||
std::vector<SlotWithTimestamp> used_slots = GetUsedSlotsWithTimestamp();
|
||||
if (std::size_t(i) > used_slots.size())
|
||||
@@ -934,7 +964,7 @@ void SaveFirstSaved(Core::System& system)
|
||||
{
|
||||
Core::RunOnCPUThread(system, [&system, lock = GetStateSaveTaskLock()] {
|
||||
// Data must reach the filesystem for up to date "UsedSlots".
|
||||
s_compress_and_dump_thread.WaitForCompletion();
|
||||
WaitForPendingSaves();
|
||||
|
||||
std::vector<SlotWithTimestamp> used_slots = GetUsedSlotsWithTimestamp();
|
||||
auto slot = GetEmptySlot(used_slots);
|
||||
@@ -945,7 +975,7 @@ void SaveFirstSaved(Core::System& system)
|
||||
slot = used_slots.front().slot;
|
||||
}
|
||||
|
||||
SaveAsFromCore(system, MakeStateFilename(*slot));
|
||||
SaveAsFromCore(system, MakeStateFilename(*slot), {});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ namespace State
|
||||
// number of states
|
||||
static const u32 NUM_STATES = 10;
|
||||
|
||||
struct SaveOptions
|
||||
{
|
||||
bool compress = true;
|
||||
bool wait_for_completion = false;
|
||||
};
|
||||
|
||||
struct StateHeaderLegacy
|
||||
{
|
||||
char game_id[6];
|
||||
@@ -92,11 +98,12 @@ u64 GetUnixTimeOfSlot(int slot);
|
||||
// These don't happen instantly - they get scheduled as events.
|
||||
// ...But only if we're not in the main CPU thread.
|
||||
// If we're in the main CPU thread then they run immediately instead.
|
||||
void Save(Core::System& system, int slot);
|
||||
void Save(Core::System& system, int slot, SaveOptions options = {});
|
||||
void Load(Core::System& system, int slot);
|
||||
|
||||
void SaveAs(Core::System& system, std::string filename);
|
||||
void SaveAs(Core::System& system, std::string filename, SaveOptions options = {});
|
||||
void LoadAs(Core::System& system, std::string filename);
|
||||
void WaitForPendingSaves();
|
||||
|
||||
void LoadLastSaved(Core::System& system, int i = 1);
|
||||
void SaveFirstSaved(Core::System& system);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 "
|
||||
""stay in place" and jump to their correct positions a few frames later."
|
||||
"<br><br>This option captures a single head pose at the XFB-copy point in FIFO order, "
|
||||
"guaranteeing all draws of a frame are coherent. It has no effect when "
|
||||
"<b>Immediately Present XFB</b> is on (that path is already coherent)."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
|
||||
static constexpr char TR_VR_GAMMA_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Adjusts gamma correction for VR eye output."
|
||||
"<br><br>1.0 = no correction (default). 2.2 = standard sRGB gamma. "
|
||||
@@ -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 <hash>-<vs|ps|gs>.txt."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
static constexpr char TR_ENABLE_OPENXR_CONFIG_SCENE_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Starts a temporary OpenXR scene while configuring an <b>OpenXR Wii Remote</b> with no "
|
||||
"game running."
|
||||
"<br><br>This keeps OpenXR controller input active so the mapping window can detect button "
|
||||
"presses and motion input outside emulation."
|
||||
"<br><br>If the temporary session cannot start, Dolphin falls back to the desktop-only "
|
||||
"mapping window."
|
||||
"<br><br>Disable this if your runtime or headset has trouble with the temporary binding "
|
||||
"scene."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
|
||||
static constexpr char TR_AR_MODE_DEBUG_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Debug aid for AR Mode development on headsets that do <b>not</b> support passthrough."
|
||||
"<br><br>Clears the per-eye OpenXR swapchain to <b>solid magenta</b> instead of "
|
||||
"transparent black, so the regions that <i>would</i> be see-through in real AR Mode "
|
||||
"become clearly visible. The OpenXR composition state is left as <code>OPAQUE</code>, "
|
||||
"so this works on any headset and never triggers "
|
||||
"<code>XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED</code>."
|
||||
"<br><br>Use this to verify that the eye framebuffer clear path, layer flag wiring, "
|
||||
"and game-geometry-on-empty-background composition all behave correctly without "
|
||||
"needing a passthrough-capable runtime."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
static constexpr char TR_AR_MODE_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Submits the OpenXR projection layer with alpha-blended composition so that empty "
|
||||
"(transparent) pixels reveal the headset passthrough feed, making game geometry float "
|
||||
"over the real world."
|
||||
"<br><br>Requires a headset and runtime that support "
|
||||
"<code>XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND</code> (e.g. Quest 3, Vive XR Elite, "
|
||||
"Varjo, HoloLens). On opaque-only PCVR headsets (Index, Vive Pro) enabling this will "
|
||||
"cause <code>xrEndFrame</code> to fail; disable it again if the VR session stops "
|
||||
"submitting frames."
|
||||
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
|
||||
|
||||
m_enable_openxr->SetDescription(tr(TR_ENABLE_OPENXR_DESCRIPTION));
|
||||
m_auto_immediate_xfb->SetDescription(tr(TR_AUTO_IMMEDIATE_XFB_DESCRIPTION));
|
||||
m_units_per_meter->SetDescription(tr(TR_UNITS_PER_METER_DESCRIPTION));
|
||||
m_lean_back_angle->SetDescription(tr(TR_LEAN_BACK_ANGLE_DESCRIPTION));
|
||||
m_camera_forward->SetDescription(tr(TR_CAMERA_FORWARD_DESCRIPTION));
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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{};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user