Merge branch 'dolphin-emu:master' into AGuy27-patch-2
This commit is contained in:
commit
eca6072ccb
|
@ -26,7 +26,7 @@ endif()
|
|||
# This is inserted into the Info.plist as well.
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15.0" CACHE STRING "")
|
||||
|
||||
set(CMAKE_USER_MAKE_RULES_OVERRIDE "CMake/FlagsOverride.cmake")
|
||||
set(CMAKE_USER_MAKE_RULES_OVERRIDE "${CMAKE_CURRENT_SOURCE_DIR}/CMake/FlagsOverride.cmake")
|
||||
|
||||
project(dolphin-emu)
|
||||
|
||||
|
@ -156,7 +156,7 @@ if(UNIX)
|
|||
endif()
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH
|
||||
${CMAKE_SOURCE_DIR}/CMake
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CMake
|
||||
)
|
||||
|
||||
# Support functions
|
||||
|
@ -788,7 +788,7 @@ if(NOT GIT_FOUND)
|
|||
endif()
|
||||
add_custom_target(
|
||||
dolphin_scmrev
|
||||
${CMAKE_COMMAND} -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} -DPROJECT_BINARY_DIR=${PROJECT_BINARY_DIR} -DDISTRIBUTOR=${DISTRIBUTOR} -DDOLPHIN_DEFAULT_UPDATE_TRACK=${DOLPHIN_DEFAULT_UPDATE_TRACK} -DGIT_FOUND=${GIT_FOUND} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} -DDOLPHIN_WC_REVISION=${DOLPHIN_WC_REVISION} -DDOLPHIN_WC_DESCRIBE=${DOLPHIN_WC_DESCRIBE} -DDOLPHIN_WC_BRANCH=${DOLPHIN_WC_BRANCH} -P ${CMAKE_SOURCE_DIR}/CMake/ScmRevGen.cmake
|
||||
${CMAKE_COMMAND} -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} -DPROJECT_BINARY_DIR=${PROJECT_BINARY_DIR} -DDISTRIBUTOR=${DISTRIBUTOR} -DDOLPHIN_DEFAULT_UPDATE_TRACK=${DOLPHIN_DEFAULT_UPDATE_TRACK} -DGIT_FOUND=${GIT_FOUND} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} -DDOLPHIN_WC_REVISION=${DOLPHIN_WC_REVISION} -DDOLPHIN_WC_DESCRIBE=${DOLPHIN_WC_DESCRIBE} -DDOLPHIN_WC_BRANCH=${DOLPHIN_WC_BRANCH} -P ${CMAKE_CURRENT_SOURCE_DIR}/CMake/ScmRevGen.cmake
|
||||
BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/Source/Core/Common/scmrev.h
|
||||
VERBATIM
|
||||
)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# G5BE4Z - Strike Force Bowling
|
||||
[Video_Hacks]
|
||||
EFBToTextureEnable = False
|
||||
DeferEFBCopies = False
|
|
@ -4,13 +4,17 @@ add_library(rcheevos
|
|||
rcheevos/include/rc_api_request.h
|
||||
rcheevos/include/rc_api_runtime.h
|
||||
rcheevos/include/rc_api_user.h
|
||||
rcheevos/include/rc_client.h
|
||||
rcheevos/include/rc_client_raintegration.h
|
||||
rcheevos/include/rc_consoles.h
|
||||
rcheevos/include/rc_error.h
|
||||
rcheevos/include/rc_export.h
|
||||
rcheevos/include/rc_hash.h
|
||||
rcheevos/include/rcheevos.h
|
||||
rcheevos/include/rc_runtime.h
|
||||
rcheevos/include/rc_runtime_types.h
|
||||
rcheevos/include/rc_url.h
|
||||
rcheevos/include/rc_util.h
|
||||
rcheevos/src/rapi/rc_api_common.c
|
||||
rcheevos/src/rapi/rc_api_common.h
|
||||
rcheevos/src/rapi/rc_api_editor.c
|
||||
|
@ -18,7 +22,6 @@ add_library(rcheevos
|
|||
rcheevos/src/rapi/rc_api_runtime.c
|
||||
rcheevos/src/rapi/rc_api_user.c
|
||||
rcheevos/src/rcheevos/alloc.c
|
||||
rcheevos/src/rcheevos/compat.c
|
||||
rcheevos/src/rcheevos/condition.c
|
||||
rcheevos/src/rcheevos/condset.c
|
||||
rcheevos/src/rcheevos/consoleinfo.c
|
||||
|
@ -26,7 +29,6 @@ add_library(rcheevos
|
|||
rcheevos/src/rcheevos/lboard.c
|
||||
rcheevos/src/rcheevos/memref.c
|
||||
rcheevos/src/rcheevos/operand.c
|
||||
rcheevos/src/rcheevos/rc_compat.h
|
||||
rcheevos/src/rcheevos/rc_internal.h
|
||||
rcheevos/src/rcheevos/rc_validate.c
|
||||
rcheevos/src/rcheevos/rc_validate.h
|
||||
|
@ -35,10 +37,23 @@ add_library(rcheevos
|
|||
rcheevos/src/rcheevos/runtime_progress.c
|
||||
rcheevos/src/rcheevos/trigger.c
|
||||
rcheevos/src/rcheevos/value.c
|
||||
rcheevos/src/rhash/aes.c
|
||||
rcheevos/src/rhash/aes.h
|
||||
rcheevos/src/rhash/cdreader.c
|
||||
rcheevos/src/rhash/hash.c
|
||||
rcheevos/src/rhash/md5.c
|
||||
rcheevos/src/rhash/md5.h
|
||||
rcheevos/src/rurl/url.c
|
||||
rcheevos/src/rc_client.c
|
||||
rcheevos/src/rc_client_external.h
|
||||
rcheevos/src/rc_client_internal.h
|
||||
rcheevos/src/rc_client_raintegration.c
|
||||
rcheevos/src/rc_client_raintegration_internal.h
|
||||
rcheevos/src/rc_compat.c
|
||||
rcheevos/src/rc_compat.h
|
||||
rcheevos/src/rc_util.c
|
||||
rcheevos/src/rc_version.c
|
||||
rcheevos/src/rc_version.h
|
||||
)
|
||||
|
||||
target_include_directories(rcheevos PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/include")
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit d9e990e6d13527532b7e2bb23164a1f3b7f33bb5
|
||||
Subproject commit b64ac2b25038bc9feb94ca759b5ba4d02642b3af
|
|
@ -22,7 +22,6 @@
|
|||
<ClCompile Include="rcheevos\src\rapi\rc_api_runtime.c" />
|
||||
<ClCompile Include="rcheevos\src\rapi\rc_api_user.c" />
|
||||
<ClCompile Include="rcheevos\src\rcheevos\alloc.c" />
|
||||
<ClCompile Include="rcheevos\src\rcheevos\compat.c" />
|
||||
<ClCompile Include="rcheevos\src\rcheevos\condition.c" />
|
||||
<ClCompile Include="rcheevos\src\rcheevos\condset.c" />
|
||||
<ClCompile Include="rcheevos\src\rcheevos\consoleinfo.c" />
|
||||
|
@ -36,9 +35,16 @@
|
|||
<ClCompile Include="rcheevos\src\rcheevos\runtime_progress.c" />
|
||||
<ClCompile Include="rcheevos\src\rcheevos\trigger.c" />
|
||||
<ClCompile Include="rcheevos\src\rcheevos\value.c" />
|
||||
<ClCompile Include="rcheevos\src\rhash\aes.c" />
|
||||
<ClCompile Include="rcheevos\src\rhash\cdreader.c" />
|
||||
<ClCompile Include="rcheevos\src\rhash\hash.c" />
|
||||
<ClCompile Include="rcheevos\src\rhash\md5.c" />
|
||||
<ClCompile Include="rcheevos\src\rurl\url.c" />
|
||||
<ClCompile Include="rcheevos\src\rc_client.c" />
|
||||
<ClCompile Include="rcheevos\src\rc_client_raintegration.c" />
|
||||
<ClCompile Include="rcheevos\src\rc_compat.c" />
|
||||
<ClCompile Include="rcheevos\src\rc_util.c" />
|
||||
<ClCompile Include="rcheevos\src\rc_version.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="rcheevos\include\rcheevos.h" />
|
||||
|
@ -47,17 +53,26 @@
|
|||
<ClInclude Include="rcheevos\include\rc_api_request.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_api_runtime.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_api_user.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_client.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_client_raintegration.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_consoles.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_error.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_export.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_hash.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_runtime.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_runtime_types.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_url.h" />
|
||||
<ClInclude Include="rcheevos\include\rc_util.h" />
|
||||
<ClInclude Include="rcheevos\src\rapi\rc_api_common.h" />
|
||||
<ClInclude Include="rcheevos\src\rcheevos\rc_compat.h" />
|
||||
<ClInclude Include="rcheevos\src\rcheevos\rc_internal.h" />
|
||||
<ClInclude Include="rcheevos\src\rcheevos\rc_validate.h" />
|
||||
<ClInclude Include="rcheevos\src\rhash\aes.h" />
|
||||
<ClInclude Include="rcheevos\src\rhash\md5.h" />
|
||||
<ClInclude Include="rcheevos\src\rc_client_external.h" />
|
||||
<ClInclude Include="rcheevos\src\rc_client_internal.h" />
|
||||
<ClInclude Include="rcheevos\src\rc_client_raintegration_internal.h" />
|
||||
<ClInclude Include="rcheevos\src\rc_compat.h" />
|
||||
<ClInclude Include="rcheevos\src\rc_version.h" />
|
||||
</ItemGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
|
|
|
@ -138,6 +138,8 @@ add_library(common
|
|||
Thread.h
|
||||
Timer.cpp
|
||||
Timer.h
|
||||
TimeUtil.cpp
|
||||
TimeUtil.h
|
||||
TraversalClient.cpp
|
||||
TraversalClient.h
|
||||
TraversalProto.h
|
||||
|
|
|
@ -75,6 +75,8 @@
|
|||
#define DUMP_AUDIO_DIR "Audio"
|
||||
#define DUMP_DSP_DIR "DSP"
|
||||
#define DUMP_SSL_DIR "SSL"
|
||||
#define DUMP_DEBUG_DIR "Debug"
|
||||
#define DUMP_DEBUG_BRANCHWATCH_DIR "BranchWatch"
|
||||
#define LOGS_DIR "Logs"
|
||||
#define MAIL_LOGS_DIR "Mail"
|
||||
#define SHADERS_DIR "Shaders"
|
||||
|
|
|
@ -856,6 +856,9 @@ static void RebuildUserDirectories(unsigned int dir_index)
|
|||
s_user_paths[D_DUMPTEXTURES_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_TEXTURES_DIR DIR_SEP;
|
||||
s_user_paths[D_DUMPDSP_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DSP_DIR DIR_SEP;
|
||||
s_user_paths[D_DUMPSSL_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_SSL_DIR DIR_SEP;
|
||||
s_user_paths[D_DUMPDEBUG_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_DIR DIR_SEP;
|
||||
s_user_paths[D_DUMPDEBUG_BRANCHWATCH_IDX] =
|
||||
s_user_paths[D_DUMPDEBUG_IDX] + DUMP_DEBUG_BRANCHWATCH_DIR DIR_SEP;
|
||||
s_user_paths[D_LOGS_IDX] = s_user_paths[D_USER_IDX] + LOGS_DIR DIR_SEP;
|
||||
s_user_paths[D_MAILLOGS_IDX] = s_user_paths[D_LOGS_IDX] + MAIL_LOGS_DIR DIR_SEP;
|
||||
s_user_paths[D_THEMES_IDX] = s_user_paths[D_USER_IDX] + THEMES_DIR DIR_SEP;
|
||||
|
@ -932,6 +935,9 @@ static void RebuildUserDirectories(unsigned int dir_index)
|
|||
s_user_paths[D_DUMPTEXTURES_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_TEXTURES_DIR DIR_SEP;
|
||||
s_user_paths[D_DUMPDSP_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DSP_DIR DIR_SEP;
|
||||
s_user_paths[D_DUMPSSL_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_SSL_DIR DIR_SEP;
|
||||
s_user_paths[D_DUMPDEBUG_IDX] = s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_DIR DIR_SEP;
|
||||
s_user_paths[D_DUMPDEBUG_BRANCHWATCH_IDX] =
|
||||
s_user_paths[D_DUMP_IDX] + DUMP_DEBUG_BRANCHWATCH_DIR DIR_SEP;
|
||||
s_user_paths[F_MEM1DUMP_IDX] = s_user_paths[D_DUMP_IDX] + MEM1_DUMP;
|
||||
s_user_paths[F_MEM2DUMP_IDX] = s_user_paths[D_DUMP_IDX] + MEM2_DUMP;
|
||||
s_user_paths[F_ARAMDUMP_IDX] = s_user_paths[D_DUMP_IDX] + ARAM_DUMP;
|
||||
|
|
|
@ -52,6 +52,8 @@ enum
|
|||
D_DUMPTEXTURES_IDX,
|
||||
D_DUMPDSP_IDX,
|
||||
D_DUMPSSL_IDX,
|
||||
D_DUMPDEBUG_IDX,
|
||||
D_DUMPDEBUG_BRANCHWATCH_IDX,
|
||||
D_LOAD_IDX,
|
||||
D_LOGS_IDX,
|
||||
D_MAILLOGS_IDX,
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Common/JsonUtil.h"
|
||||
|
||||
picojson::object ToJsonObject(const Common::Vec3& vec)
|
||||
{
|
||||
picojson::object obj;
|
||||
obj.emplace("x", vec.x);
|
||||
obj.emplace("y", vec.y);
|
||||
obj.emplace("z", vec.z);
|
||||
return obj;
|
||||
}
|
||||
|
||||
void FromJson(const picojson::object& obj, Common::Vec3& vec)
|
||||
{
|
||||
vec.x = ReadNumericOrDefault<float>(obj, "x");
|
||||
vec.y = ReadNumericOrDefault<float>(obj, "y");
|
||||
vec.z = ReadNumericOrDefault<float>(obj, "z");
|
||||
}
|
|
@ -3,10 +3,13 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include <picojson.h>
|
||||
|
||||
#include "Common/MathUtil.h"
|
||||
#include "Common/Matrix.h"
|
||||
|
||||
// Ideally this would use a concept like, 'template <std::ranges::range Range>' to constrain it,
|
||||
// but unfortunately we'd need to require clang 15 for that, since the ranges library isn't
|
||||
// fully implemented until then, but this should suffice.
|
||||
|
@ -24,3 +27,18 @@ picojson::array ToJsonArray(const Range& data)
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
Type ReadNumericOrDefault(const picojson::object& obj, const std::string& key,
|
||||
Type default_value = Type{})
|
||||
{
|
||||
const auto it = obj.find(key);
|
||||
if (it == obj.end())
|
||||
return default_value;
|
||||
if (!it->second.is<double>())
|
||||
return default_value;
|
||||
return MathUtil::SaturatingCast<Type>(it->second.get<double>());
|
||||
}
|
||||
|
||||
picojson::object ToJsonObject(const Common::Vec3& vec);
|
||||
void FromJson(const picojson::object& obj, Common::Vec3& vec);
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Common/TimeUtil.h"
|
||||
|
||||
#include <ctime>
|
||||
#include <optional>
|
||||
|
||||
namespace Common
|
||||
{
|
||||
std::optional<std::tm> Localtime(std::time_t time)
|
||||
{
|
||||
std::tm local_time;
|
||||
#ifdef _MSC_VER
|
||||
if (localtime_s(&local_time, &time) != 0)
|
||||
return std::nullopt;
|
||||
#else
|
||||
std::tm* result = localtime_r(&time, &local_time);
|
||||
if (result != &local_time)
|
||||
return std::nullopt;
|
||||
#endif
|
||||
return local_time;
|
||||
}
|
||||
} // Namespace Common
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ctime>
|
||||
#include <optional>
|
||||
|
||||
namespace Common
|
||||
{
|
||||
// Threadsafe and error-checking variant of std::localtime()
|
||||
std::optional<std::tm> Localtime(std::time_t time);
|
||||
} // Namespace Common
|
|
@ -24,8 +24,6 @@
|
|||
#include "VideoCommon/OnScreenDisplay.h"
|
||||
#include "VideoCommon/VideoEvents.h"
|
||||
|
||||
static constexpr bool hardcore_mode_enabled = false;
|
||||
|
||||
static std::unique_ptr<OSD::Icon> DecodeBadgeToOSDIcon(const AchievementManager::Badge& badge);
|
||||
|
||||
AchievementManager& AchievementManager::GetInstance()
|
||||
|
@ -359,11 +357,11 @@ void AchievementManager::ActivateDeactivateAchievements()
|
|||
bool encore = Config::Get(Config::RA_ENCORE_ENABLED);
|
||||
for (u32 ix = 0; ix < m_game_data.num_achievements; ix++)
|
||||
{
|
||||
u32 points = (m_game_data.achievements[ix].category == RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ?
|
||||
0 :
|
||||
m_game_data.achievements[ix].points;
|
||||
auto iter = m_unlock_map.insert(
|
||||
{m_game_data.achievements[ix].id, UnlockStatus{.game_data_index = ix, .points = points}});
|
||||
auto iter =
|
||||
m_unlock_map.insert({m_game_data.achievements[ix].id,
|
||||
UnlockStatus{.game_data_index = ix,
|
||||
.points = m_game_data.achievements[ix].points,
|
||||
.category = m_game_data.achievements[ix].category}});
|
||||
ActivateDeactivateAchievement(iter.first->first, enabled, unofficial, encore);
|
||||
}
|
||||
INFO_LOG_FMT(ACHIEVEMENTS, "Achievements (de)activated.");
|
||||
|
@ -373,12 +371,13 @@ void AchievementManager::ActivateDeactivateLeaderboards()
|
|||
{
|
||||
if (!Config::Get(Config::RA_ENABLED) || !IsLoggedIn())
|
||||
return;
|
||||
bool leaderboards_enabled = Config::Get(Config::RA_LEADERBOARDS_ENABLED);
|
||||
bool leaderboards_enabled =
|
||||
Config::Get(Config::RA_LEADERBOARDS_ENABLED) && Config::Get(Config::RA_HARDCORE_ENABLED);
|
||||
for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++)
|
||||
{
|
||||
auto leaderboard = m_game_data.leaderboards[ix];
|
||||
u32 leaderboard_id = leaderboard.id;
|
||||
if (m_is_game_loaded && leaderboards_enabled && hardcore_mode_enabled)
|
||||
if (m_is_game_loaded && leaderboards_enabled)
|
||||
{
|
||||
rc_runtime_activate_lboard(&m_runtime, leaderboard_id, leaderboard.definition, nullptr, 0);
|
||||
m_queue.EmplaceItem([this, leaderboard_id] {
|
||||
|
@ -752,8 +751,6 @@ void AchievementManager::AchievementEventHandler(const rc_runtime_event_t* runti
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_update_callback();
|
||||
}
|
||||
|
||||
std::recursive_mutex& AchievementManager::GetLock()
|
||||
|
@ -798,8 +795,11 @@ AchievementManager::PointSpread AchievementManager::TallyScore() const
|
|||
PointSpread spread{};
|
||||
if (!IsGameLoaded())
|
||||
return spread;
|
||||
bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED);
|
||||
for (const auto& entry : m_unlock_map)
|
||||
{
|
||||
if (entry.second.category != RC_ACHIEVEMENT_CATEGORY_CORE)
|
||||
continue;
|
||||
u32 points = entry.second.points;
|
||||
spread.total_count++;
|
||||
spread.total_points += points;
|
||||
|
@ -884,10 +884,14 @@ void AchievementManager::SetDisabled(bool disable)
|
|||
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager has been disabled.");
|
||||
OSD::AddMessage("Please close all games to re-enable achievements.", OSD::Duration::VERY_LONG,
|
||||
OSD::Color::RED);
|
||||
m_update_callback();
|
||||
}
|
||||
|
||||
if (previously_disabled && !disable)
|
||||
{
|
||||
INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager has been re-enabled.");
|
||||
m_update_callback();
|
||||
}
|
||||
};
|
||||
|
||||
const AchievementManager::NamedIconMap& AchievementManager::GetChallengeIcons() const
|
||||
|
@ -902,6 +906,7 @@ void AchievementManager::CloseGame()
|
|||
if (m_is_game_loaded)
|
||||
{
|
||||
m_is_game_loaded = false;
|
||||
m_active_challenges.clear();
|
||||
ActivateDeactivateAchievements();
|
||||
ActivateDeactivateLeaderboards();
|
||||
ActivateDeactivateRichPresence();
|
||||
|
@ -926,6 +931,7 @@ void AchievementManager::Logout()
|
|||
{
|
||||
std::lock_guard lg{m_lock};
|
||||
CloseGame();
|
||||
SetDisabled(false);
|
||||
m_player_badge.name.clear();
|
||||
Config::SetBaseOrCurrent(Config::RA_API_TOKEN, "");
|
||||
}
|
||||
|
@ -937,6 +943,7 @@ void AchievementManager::Logout()
|
|||
void AchievementManager::Shutdown()
|
||||
{
|
||||
CloseGame();
|
||||
SetDisabled(false);
|
||||
m_is_runtime_initialized = false;
|
||||
m_queue.Shutdown();
|
||||
// DON'T log out - keep those credentials for next run.
|
||||
|
@ -1295,6 +1302,7 @@ void AchievementManager::ActivateDeactivateAchievement(AchievementId id, bool en
|
|||
const UnlockStatus& status = it->second;
|
||||
u32 index = status.game_data_index;
|
||||
bool active = (rc_runtime_get_achievement(&m_runtime, id) != nullptr);
|
||||
bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED);
|
||||
|
||||
// Deactivate achievements if game is not loaded
|
||||
bool activate = m_is_game_loaded;
|
||||
|
@ -1349,6 +1357,7 @@ AchievementManager::ResponseType AchievementManager::AwardAchievement(Achievemen
|
|||
{
|
||||
std::string username = Config::Get(Config::RA_USERNAME);
|
||||
std::string api_token = Config::Get(Config::RA_API_TOKEN);
|
||||
bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED);
|
||||
rc_api_award_achievement_request_t award_request = {.username = username.c_str(),
|
||||
.api_token = api_token.c_str(),
|
||||
.achievement_id = achievement_id,
|
||||
|
@ -1418,7 +1427,7 @@ void AchievementManager::DisplayWelcomeMessage()
|
|||
{
|
||||
std::lock_guard lg{m_lock};
|
||||
PointSpread spread = TallyScore();
|
||||
if (hardcore_mode_enabled)
|
||||
if (Config::Get(Config::RA_HARDCORE_ENABLED))
|
||||
{
|
||||
OSD::AddMessage(
|
||||
fmt::format("You have {}/{} achievements worth {}/{} points", spread.hard_unlocks,
|
||||
|
@ -1443,6 +1452,7 @@ void AchievementManager::DisplayWelcomeMessage()
|
|||
|
||||
void AchievementManager::HandleAchievementTriggeredEvent(const rc_runtime_event_t* runtime_event)
|
||||
{
|
||||
bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED);
|
||||
const auto event_id = runtime_event->id;
|
||||
auto it = m_unlock_map.find(event_id);
|
||||
if (it == m_unlock_map.end())
|
||||
|
@ -1451,7 +1461,6 @@ void AchievementManager::HandleAchievementTriggeredEvent(const rc_runtime_event_
|
|||
return;
|
||||
}
|
||||
it->second.session_unlock_count++;
|
||||
m_queue.EmplaceItem([this, event_id] { AwardAchievement(event_id); });
|
||||
AchievementId game_data_index = it->second.game_data_index;
|
||||
OSD::AddMessage(fmt::format("Unlocked: {} ({})", m_game_data.achievements[game_data_index].title,
|
||||
m_game_data.achievements[game_data_index].points),
|
||||
|
@ -1460,22 +1469,28 @@ void AchievementManager::HandleAchievementTriggeredEvent(const rc_runtime_event_
|
|||
(Config::Get(Config::RA_BADGES_ENABLED)) ?
|
||||
DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge) :
|
||||
nullptr);
|
||||
PointSpread spread = TallyScore();
|
||||
if (spread.hard_points == spread.total_points)
|
||||
if (m_game_data.achievements[game_data_index].category == RC_ACHIEVEMENT_CATEGORY_CORE)
|
||||
{
|
||||
OSD::AddMessage(
|
||||
fmt::format("Congratulations! {} has mastered {}", m_display_name, m_game_data.title),
|
||||
OSD::Duration::VERY_LONG, OSD::Color::YELLOW,
|
||||
(Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) :
|
||||
nullptr);
|
||||
}
|
||||
else if (spread.hard_points + spread.soft_points == spread.total_points)
|
||||
{
|
||||
OSD::AddMessage(
|
||||
fmt::format("Congratulations! {} has completed {}", m_display_name, m_game_data.title),
|
||||
OSD::Duration::VERY_LONG, OSD::Color::CYAN,
|
||||
(Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) :
|
||||
nullptr);
|
||||
m_queue.EmplaceItem([this, event_id] { AwardAchievement(event_id); });
|
||||
PointSpread spread = TallyScore();
|
||||
if (spread.hard_points == spread.total_points &&
|
||||
it->second.remote_unlock_status != UnlockStatus::UnlockType::HARDCORE)
|
||||
{
|
||||
OSD::AddMessage(
|
||||
fmt::format("Congratulations! {} has mastered {}", m_display_name, m_game_data.title),
|
||||
OSD::Duration::VERY_LONG, OSD::Color::YELLOW,
|
||||
(Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) :
|
||||
nullptr);
|
||||
}
|
||||
else if (spread.hard_points + spread.soft_points == spread.total_points &&
|
||||
it->second.remote_unlock_status == UnlockStatus::UnlockType::LOCKED)
|
||||
{
|
||||
OSD::AddMessage(
|
||||
fmt::format("Congratulations! {} has completed {}", m_display_name, m_game_data.title),
|
||||
OSD::Duration::VERY_LONG, OSD::Color::CYAN,
|
||||
(Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) :
|
||||
nullptr);
|
||||
}
|
||||
}
|
||||
ActivateDeactivateAchievement(event_id, Config::Get(Config::RA_ACHIEVEMENTS_ENABLED),
|
||||
Config::Get(Config::RA_UNOFFICIAL_ENABLED),
|
||||
|
|
|
@ -89,6 +89,7 @@ public:
|
|||
u32 points = 0;
|
||||
BadgeStatus locked_badge;
|
||||
BadgeStatus unlocked_badge;
|
||||
u32 category = RC_ACHIEVEMENT_CATEGORY_CORE;
|
||||
};
|
||||
|
||||
static constexpr std::string_view GRAY = "transparent";
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "Core/Config/MainSettings.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
#include "Core/HW/DVD/DVDInterface.h"
|
||||
#include "Core/HW/EXI/EXI_DeviceIPL.h"
|
||||
|
@ -158,6 +159,11 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard
|
|||
|
||||
auto& ppc_state = system.GetPPCState();
|
||||
auto& mmu = system.GetMMU();
|
||||
auto& branch_watch = system.GetPowerPC().GetBranchWatch();
|
||||
|
||||
const bool resume_branch_watch = branch_watch.GetRecordingActive();
|
||||
if (system.IsBranchWatchIgnoreApploader())
|
||||
branch_watch.Pause();
|
||||
|
||||
// Call iAppLoaderEntry.
|
||||
DEBUG_LOG_FMT(BOOT, "Call iAppLoaderEntry");
|
||||
|
@ -220,6 +226,8 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard
|
|||
// return
|
||||
ppc_state.pc = ppc_state.gpr[3];
|
||||
|
||||
branch_watch.SetRecordingActive(resume_branch_watch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,8 @@ add_library(core
|
|||
CoreTiming.h
|
||||
CPUThreadConfigCallback.cpp
|
||||
CPUThreadConfigCallback.h
|
||||
Debugger/BranchWatch.cpp
|
||||
Debugger/BranchWatch.h
|
||||
Debugger/CodeTrace.cpp
|
||||
Debugger/CodeTrace.h
|
||||
Debugger/DebugInterface.h
|
||||
|
|
|
@ -170,7 +170,8 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri
|
|||
return;
|
||||
|
||||
#ifdef USE_RETRO_ACHIEVEMENTS
|
||||
AchievementManager::GetInstance().SetDisabled(true);
|
||||
if (game_id != "00000000")
|
||||
AchievementManager::GetInstance().SetDisabled(true);
|
||||
#endif // USE_RETRO_ACHIEVEMENTS
|
||||
|
||||
if (game_id == "00000000")
|
||||
|
|
|
@ -291,6 +291,7 @@ void Stop() // - Hammertime!
|
|||
|
||||
#ifdef USE_RETRO_ACHIEVEMENTS
|
||||
AchievementManager::GetInstance().CloseGame();
|
||||
AchievementManager::GetInstance().SetDisabled(false);
|
||||
#endif // USE_RETRO_ACHIEVEMENTS
|
||||
|
||||
s_is_stopping = true;
|
||||
|
|
|
@ -0,0 +1,314 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/BitField.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/PowerPC/Gekko.h"
|
||||
#include "Core/PowerPC/MMU.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
void BranchWatch::Clear(const CPUThreadGuard&)
|
||||
{
|
||||
m_selection.clear();
|
||||
m_collection_vt.clear();
|
||||
m_collection_vf.clear();
|
||||
m_collection_pt.clear();
|
||||
m_collection_pf.clear();
|
||||
m_recording_phase = Phase::Blacklist;
|
||||
m_blacklist_size = 0;
|
||||
}
|
||||
|
||||
// This is a bitfield aggregate of metadata required to reconstruct a BranchWatch's Collections and
|
||||
// Selection from a text file (a snapshot). For maximum forward compatibility, should that ever be
|
||||
// required, the StorageType is an unsigned long long instead of something more reasonable like an
|
||||
// unsigned int or u8. This is because the snapshot text file format contains no version info.
|
||||
union USnapshotMetadata
|
||||
{
|
||||
using Inspection = BranchWatch::SelectionInspection;
|
||||
using StorageType = unsigned long long;
|
||||
|
||||
static_assert(Inspection::EndOfEnumeration == Inspection{(1u << 3) + 1});
|
||||
|
||||
StorageType hex;
|
||||
|
||||
BitField<0, 1, bool, StorageType> is_virtual;
|
||||
BitField<1, 1, bool, StorageType> condition;
|
||||
BitField<2, 1, bool, StorageType> is_selected;
|
||||
BitField<3, 4, Inspection, StorageType> inspection;
|
||||
|
||||
USnapshotMetadata() : hex(0) {}
|
||||
explicit USnapshotMetadata(bool is_virtual_, bool condition_, bool is_selected_,
|
||||
Inspection inspection_)
|
||||
: USnapshotMetadata()
|
||||
{
|
||||
is_virtual = is_virtual_;
|
||||
condition = condition_;
|
||||
is_selected = is_selected_;
|
||||
inspection = inspection_;
|
||||
}
|
||||
};
|
||||
|
||||
void BranchWatch::Save(const CPUThreadGuard& guard, std::FILE* file) const
|
||||
{
|
||||
if (!CanSave())
|
||||
{
|
||||
ASSERT_MSG(CORE, false, "BranchWatch can not be saved.");
|
||||
return;
|
||||
}
|
||||
if (file == nullptr)
|
||||
return;
|
||||
|
||||
const auto routine = [&](const Collection& collection, bool is_virtual, bool condition) {
|
||||
for (const Collection::value_type& kv : collection)
|
||||
{
|
||||
const auto iter = std::find_if(
|
||||
m_selection.begin(), m_selection.end(),
|
||||
[&](const Selection::value_type& value) { return value.collection_ptr == &kv; });
|
||||
fmt::println(file, "{:08x} {:08x} {:08x} {} {} {:x}", kv.first.origin_addr,
|
||||
kv.first.destin_addr, kv.first.original_inst.hex, kv.second.total_hits,
|
||||
kv.second.hits_snapshot,
|
||||
iter == m_selection.end() ?
|
||||
USnapshotMetadata(is_virtual, condition, false, {}).hex :
|
||||
USnapshotMetadata(is_virtual, condition, true, iter->inspection).hex);
|
||||
}
|
||||
};
|
||||
routine(m_collection_vt, true, true);
|
||||
routine(m_collection_pt, false, true);
|
||||
routine(m_collection_vf, true, false);
|
||||
routine(m_collection_pf, false, false);
|
||||
}
|
||||
|
||||
void BranchWatch::Load(const CPUThreadGuard& guard, std::FILE* file)
|
||||
{
|
||||
if (file == nullptr)
|
||||
return;
|
||||
|
||||
Clear(guard);
|
||||
|
||||
u32 origin_addr, destin_addr, inst_hex;
|
||||
std::size_t total_hits, hits_snapshot;
|
||||
USnapshotMetadata snapshot_metadata = {};
|
||||
while (std::fscanf(file, "%x %x %x %zu %zu %llx", &origin_addr, &destin_addr, &inst_hex,
|
||||
&total_hits, &hits_snapshot, &snapshot_metadata.hex) == 6)
|
||||
{
|
||||
const bool is_virtual = snapshot_metadata.is_virtual;
|
||||
const bool condition = snapshot_metadata.condition;
|
||||
|
||||
const auto [kv_iter, emplace_success] =
|
||||
GetCollection(is_virtual, condition)
|
||||
.try_emplace({{origin_addr, destin_addr}, inst_hex},
|
||||
BranchWatchCollectionValue{total_hits, hits_snapshot});
|
||||
|
||||
if (!emplace_success)
|
||||
continue;
|
||||
|
||||
if (snapshot_metadata.is_selected)
|
||||
{
|
||||
// TODO C++20: Parenthesized initialization of aggregates has bad compiler support.
|
||||
m_selection.emplace_back(BranchWatchSelectionValueType{&*kv_iter, is_virtual, condition,
|
||||
snapshot_metadata.inspection});
|
||||
}
|
||||
else if (hits_snapshot != 0)
|
||||
{
|
||||
++m_blacklist_size; // This will be very wrong when not in Blacklist mode. That's ok.
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_selection.empty())
|
||||
m_recording_phase = Phase::Reduction;
|
||||
}
|
||||
|
||||
void BranchWatch::IsolateHasExecuted(const CPUThreadGuard&)
|
||||
{
|
||||
switch (m_recording_phase)
|
||||
{
|
||||
case Phase::Blacklist:
|
||||
{
|
||||
m_selection.reserve(GetCollectionSize() - m_blacklist_size);
|
||||
const auto routine = [&](Collection& collection, bool is_virtual, bool condition) {
|
||||
for (Collection::value_type& kv : collection)
|
||||
{
|
||||
if (kv.second.hits_snapshot == 0)
|
||||
{
|
||||
// TODO C++20: Parenthesized initialization of aggregates has bad compiler support.
|
||||
m_selection.emplace_back(
|
||||
BranchWatchSelectionValueType{&kv, is_virtual, condition, SelectionInspection{}});
|
||||
kv.second.hits_snapshot = kv.second.total_hits;
|
||||
}
|
||||
}
|
||||
};
|
||||
routine(m_collection_vt, true, true);
|
||||
routine(m_collection_vf, true, false);
|
||||
routine(m_collection_pt, false, true);
|
||||
routine(m_collection_pf, false, false);
|
||||
m_recording_phase = Phase::Reduction;
|
||||
return;
|
||||
}
|
||||
case Phase::Reduction:
|
||||
std::erase_if(m_selection, [](const Selection::value_type& value) -> bool {
|
||||
Collection::value_type* const kv = value.collection_ptr;
|
||||
if (kv->second.total_hits == kv->second.hits_snapshot)
|
||||
return true;
|
||||
kv->second.hits_snapshot = kv->second.total_hits;
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BranchWatch::IsolateNotExecuted(const CPUThreadGuard&)
|
||||
{
|
||||
switch (m_recording_phase)
|
||||
{
|
||||
case Phase::Blacklist:
|
||||
{
|
||||
const auto routine = [&](Collection& collection) {
|
||||
for (Collection::value_type& kv : collection)
|
||||
kv.second.hits_snapshot = kv.second.total_hits;
|
||||
};
|
||||
routine(m_collection_vt);
|
||||
routine(m_collection_vf);
|
||||
routine(m_collection_pt);
|
||||
routine(m_collection_pf);
|
||||
m_blacklist_size = GetCollectionSize();
|
||||
return;
|
||||
}
|
||||
case Phase::Reduction:
|
||||
std::erase_if(m_selection, [](const Selection::value_type& value) -> bool {
|
||||
Collection::value_type* const kv = value.collection_ptr;
|
||||
if (kv->second.total_hits != kv->second.hits_snapshot)
|
||||
return true;
|
||||
kv->second.hits_snapshot = kv->second.total_hits;
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BranchWatch::IsolateWasOverwritten(const CPUThreadGuard& guard)
|
||||
{
|
||||
if (Core::GetState() == Core::State::Uninitialized)
|
||||
{
|
||||
ASSERT_MSG(CORE, false, "Core is uninitialized.");
|
||||
return;
|
||||
}
|
||||
switch (m_recording_phase)
|
||||
{
|
||||
case Phase::Blacklist:
|
||||
{
|
||||
// This is a dirty hack of the assumptions that make the blacklist phase work. If the
|
||||
// hits_snapshot is non-zero while in the blacklist phase, that means it has been marked
|
||||
// for exclusion from the transition to the reduction phase.
|
||||
const auto routine = [&](Collection& collection, PowerPC::RequestedAddressSpace address_space) {
|
||||
for (Collection::value_type& kv : collection)
|
||||
{
|
||||
if (kv.second.hits_snapshot == 0)
|
||||
{
|
||||
const std::optional read_result =
|
||||
PowerPC::MMU::HostTryReadInstruction(guard, kv.first.origin_addr, address_space);
|
||||
if (!read_result.has_value())
|
||||
continue;
|
||||
if (kv.first.original_inst.hex == read_result->value)
|
||||
kv.second.hits_snapshot = ++m_blacklist_size; // Any non-zero number will work.
|
||||
}
|
||||
}
|
||||
};
|
||||
routine(m_collection_vt, PowerPC::RequestedAddressSpace::Virtual);
|
||||
routine(m_collection_vf, PowerPC::RequestedAddressSpace::Virtual);
|
||||
routine(m_collection_pt, PowerPC::RequestedAddressSpace::Physical);
|
||||
routine(m_collection_pf, PowerPC::RequestedAddressSpace::Physical);
|
||||
return;
|
||||
}
|
||||
case Phase::Reduction:
|
||||
std::erase_if(m_selection, [&guard](const Selection::value_type& value) -> bool {
|
||||
const std::optional read_result = PowerPC::MMU::HostTryReadInstruction(
|
||||
guard, value.collection_ptr->first.origin_addr,
|
||||
value.is_virtual ? PowerPC::RequestedAddressSpace::Virtual :
|
||||
PowerPC::RequestedAddressSpace::Physical);
|
||||
if (!read_result.has_value())
|
||||
return false;
|
||||
return value.collection_ptr->first.original_inst.hex == read_result->value;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BranchWatch::IsolateNotOverwritten(const CPUThreadGuard& guard)
|
||||
{
|
||||
if (Core::GetState() == Core::State::Uninitialized)
|
||||
{
|
||||
ASSERT_MSG(CORE, false, "Core is uninitialized.");
|
||||
return;
|
||||
}
|
||||
switch (m_recording_phase)
|
||||
{
|
||||
case Phase::Blacklist:
|
||||
{
|
||||
// Same dirty hack with != rather than ==, see above for details
|
||||
const auto routine = [&](Collection& collection, PowerPC::RequestedAddressSpace address_space) {
|
||||
for (Collection::value_type& kv : collection)
|
||||
if (kv.second.hits_snapshot == 0)
|
||||
{
|
||||
const std::optional read_result =
|
||||
PowerPC::MMU::HostTryReadInstruction(guard, kv.first.origin_addr, address_space);
|
||||
if (!read_result.has_value())
|
||||
continue;
|
||||
if (kv.first.original_inst.hex != read_result->value)
|
||||
kv.second.hits_snapshot = ++m_blacklist_size; // Any non-zero number will work.
|
||||
}
|
||||
};
|
||||
routine(m_collection_vt, PowerPC::RequestedAddressSpace::Virtual);
|
||||
routine(m_collection_vf, PowerPC::RequestedAddressSpace::Virtual);
|
||||
routine(m_collection_pt, PowerPC::RequestedAddressSpace::Physical);
|
||||
routine(m_collection_pf, PowerPC::RequestedAddressSpace::Physical);
|
||||
return;
|
||||
}
|
||||
case Phase::Reduction:
|
||||
std::erase_if(m_selection, [&guard](const Selection::value_type& value) -> bool {
|
||||
const std::optional read_result = PowerPC::MMU::HostTryReadInstruction(
|
||||
guard, value.collection_ptr->first.origin_addr,
|
||||
value.is_virtual ? PowerPC::RequestedAddressSpace::Virtual :
|
||||
PowerPC::RequestedAddressSpace::Physical);
|
||||
if (!read_result.has_value())
|
||||
return false;
|
||||
return value.collection_ptr->first.original_inst.hex != read_result->value;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BranchWatch::UpdateHitsSnapshot()
|
||||
{
|
||||
switch (m_recording_phase)
|
||||
{
|
||||
case Phase::Reduction:
|
||||
for (Selection::value_type& value : m_selection)
|
||||
value.collection_ptr->second.hits_snapshot = value.collection_ptr->second.total_hits;
|
||||
return;
|
||||
case Phase::Blacklist:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BranchWatch::ClearSelectionInspection()
|
||||
{
|
||||
std::for_each(m_selection.begin(), m_selection.end(),
|
||||
[](Selection::value_type& value) { value.inspection = {}; });
|
||||
}
|
||||
|
||||
void BranchWatch::SetSelectedInspected(std::size_t idx, SelectionInspection inspection)
|
||||
{
|
||||
m_selection[idx].inspection |= inspection;
|
||||
}
|
||||
} // namespace Core
|
|
@ -0,0 +1,278 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/BitUtils.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/EnumUtils.h"
|
||||
#include "Core/PowerPC/Gekko.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class CPUThreadGuard;
|
||||
}
|
||||
|
||||
namespace Core
|
||||
{
|
||||
struct FakeBranchWatchCollectionKey
|
||||
{
|
||||
u32 origin_addr;
|
||||
u32 destin_addr;
|
||||
|
||||
// TODO C++20: constexpr w/ std::bit_cast
|
||||
inline operator u64() const { return Common::BitCast<u64>(*this); }
|
||||
};
|
||||
struct BranchWatchCollectionKey : FakeBranchWatchCollectionKey
|
||||
{
|
||||
UGeckoInstruction original_inst;
|
||||
};
|
||||
struct BranchWatchCollectionValue
|
||||
{
|
||||
std::size_t total_hits = 0;
|
||||
std::size_t hits_snapshot = 0;
|
||||
};
|
||||
} // namespace Core
|
||||
|
||||
template <>
|
||||
struct std::hash<Core::BranchWatchCollectionKey>
|
||||
{
|
||||
std::size_t operator()(const Core::BranchWatchCollectionKey& s) const noexcept
|
||||
{
|
||||
return std::hash<u64>{}(static_cast<const Core::FakeBranchWatchCollectionKey&>(s));
|
||||
}
|
||||
};
|
||||
|
||||
namespace Core
|
||||
{
|
||||
inline bool operator==(const BranchWatchCollectionKey& lhs,
|
||||
const BranchWatchCollectionKey& rhs) noexcept
|
||||
{
|
||||
const std::hash<BranchWatchCollectionKey> hash;
|
||||
return hash(lhs) == hash(rhs) && lhs.original_inst.hex == rhs.original_inst.hex;
|
||||
}
|
||||
|
||||
enum class BranchWatchSelectionInspection : u8
|
||||
{
|
||||
SetOriginNOP = 1u << 0,
|
||||
SetDestinBLR = 1u << 1,
|
||||
SetOriginSymbolBLR = 1u << 2,
|
||||
SetDestinSymbolBLR = 1u << 3,
|
||||
EndOfEnumeration,
|
||||
};
|
||||
|
||||
constexpr BranchWatchSelectionInspection operator|(BranchWatchSelectionInspection lhs,
|
||||
BranchWatchSelectionInspection rhs)
|
||||
{
|
||||
return static_cast<BranchWatchSelectionInspection>(Common::ToUnderlying(lhs) |
|
||||
Common::ToUnderlying(rhs));
|
||||
}
|
||||
|
||||
constexpr BranchWatchSelectionInspection operator&(BranchWatchSelectionInspection lhs,
|
||||
BranchWatchSelectionInspection rhs)
|
||||
{
|
||||
return static_cast<BranchWatchSelectionInspection>(Common::ToUnderlying(lhs) &
|
||||
Common::ToUnderlying(rhs));
|
||||
}
|
||||
|
||||
constexpr BranchWatchSelectionInspection& operator|=(BranchWatchSelectionInspection& self,
|
||||
BranchWatchSelectionInspection other)
|
||||
{
|
||||
return self = self | other;
|
||||
}
|
||||
|
||||
using BranchWatchCollection =
|
||||
std::unordered_map<BranchWatchCollectionKey, BranchWatchCollectionValue>;
|
||||
|
||||
struct BranchWatchSelectionValueType
|
||||
{
|
||||
using Inspection = BranchWatchSelectionInspection;
|
||||
|
||||
BranchWatchCollection::value_type* collection_ptr;
|
||||
bool is_virtual;
|
||||
bool condition;
|
||||
// This is moreso a GUI thing, but it works best in the Core code for multiple reasons.
|
||||
Inspection inspection;
|
||||
};
|
||||
|
||||
using BranchWatchSelection = std::vector<BranchWatchSelectionValueType>;
|
||||
|
||||
enum class BranchWatchPhase : bool
|
||||
{
|
||||
Blacklist,
|
||||
Reduction,
|
||||
};
|
||||
|
||||
class BranchWatch final // Class is final to enforce the safety of GetOffsetOfRecordingActive().
|
||||
{
|
||||
public:
|
||||
using Collection = BranchWatchCollection;
|
||||
using Selection = BranchWatchSelection;
|
||||
using Phase = BranchWatchPhase;
|
||||
using SelectionInspection = BranchWatchSelectionInspection;
|
||||
|
||||
bool GetRecordingActive() const { return m_recording_active; }
|
||||
void SetRecordingActive(bool active) { m_recording_active = active; }
|
||||
void Start() { SetRecordingActive(true); }
|
||||
void Pause() { SetRecordingActive(false); }
|
||||
void Clear(const CPUThreadGuard& guard);
|
||||
|
||||
void Save(const CPUThreadGuard& guard, std::FILE* file) const;
|
||||
void Load(const CPUThreadGuard& guard, std::FILE* file);
|
||||
|
||||
void IsolateHasExecuted(const CPUThreadGuard& guard);
|
||||
void IsolateNotExecuted(const CPUThreadGuard& guard);
|
||||
void IsolateWasOverwritten(const CPUThreadGuard& guard);
|
||||
void IsolateNotOverwritten(const CPUThreadGuard& guard);
|
||||
void UpdateHitsSnapshot();
|
||||
void ClearSelectionInspection();
|
||||
void SetSelectedInspected(std::size_t idx, SelectionInspection inspection);
|
||||
|
||||
Selection& GetSelection() { return m_selection; }
|
||||
const Selection& GetSelection() const { return m_selection; }
|
||||
|
||||
std::size_t GetCollectionSize() const
|
||||
{
|
||||
return m_collection_vt.size() + m_collection_vf.size() + m_collection_pt.size() +
|
||||
m_collection_pf.size();
|
||||
}
|
||||
std::size_t GetBlacklistSize() const { return m_blacklist_size; }
|
||||
Phase GetRecordingPhase() const { return m_recording_phase; };
|
||||
|
||||
// An empty selection in reduction mode can't be reconstructed when loading from a file.
|
||||
bool CanSave() const { return !(m_recording_phase == Phase::Reduction && m_selection.empty()); }
|
||||
|
||||
// All Hit member functions are for the CPUThread only. The static ones are static to remain
|
||||
// compatible with the JITs' ABI_CallFunction function, which doesn't support non-static member
|
||||
// functions. HitXX_fk are optimized for when origin and destination can be passed in one register
|
||||
// easily as a Core::FakeBranchWatchCollectionKey (abbreviated as "fk"). HitXX_fk_n are the same,
|
||||
// but also increment the total_hits by N (see dcbx JIT code).
|
||||
static void HitVirtualTrue_fk(BranchWatch* branch_watch, u64 fake_key, u32 inst)
|
||||
{
|
||||
branch_watch->m_collection_vt[{Common::BitCast<FakeBranchWatchCollectionKey>(fake_key), inst}]
|
||||
.total_hits += 1;
|
||||
}
|
||||
|
||||
static void HitPhysicalTrue_fk(BranchWatch* branch_watch, u64 fake_key, u32 inst)
|
||||
{
|
||||
branch_watch->m_collection_pt[{Common::BitCast<FakeBranchWatchCollectionKey>(fake_key), inst}]
|
||||
.total_hits += 1;
|
||||
}
|
||||
|
||||
static void HitVirtualFalse_fk(BranchWatch* branch_watch, u64 fake_key, u32 inst)
|
||||
{
|
||||
branch_watch->m_collection_vf[{Common::BitCast<FakeBranchWatchCollectionKey>(fake_key), inst}]
|
||||
.total_hits += 1;
|
||||
}
|
||||
|
||||
static void HitPhysicalFalse_fk(BranchWatch* branch_watch, u64 fake_key, u32 inst)
|
||||
{
|
||||
branch_watch->m_collection_pf[{Common::BitCast<FakeBranchWatchCollectionKey>(fake_key), inst}]
|
||||
.total_hits += 1;
|
||||
}
|
||||
|
||||
static void HitVirtualTrue_fk_n(BranchWatch* branch_watch, u64 fake_key, u32 inst, u32 n)
|
||||
{
|
||||
branch_watch->m_collection_vt[{Common::BitCast<FakeBranchWatchCollectionKey>(fake_key), inst}]
|
||||
.total_hits += n;
|
||||
}
|
||||
|
||||
static void HitPhysicalTrue_fk_n(BranchWatch* branch_watch, u64 fake_key, u32 inst, u32 n)
|
||||
{
|
||||
branch_watch->m_collection_pt[{Common::BitCast<FakeBranchWatchCollectionKey>(fake_key), inst}]
|
||||
.total_hits += n;
|
||||
}
|
||||
|
||||
// HitVirtualFalse_fk_n and HitPhysicalFalse_fk_n are never used, so they are omitted here.
|
||||
|
||||
static void HitVirtualTrue(BranchWatch* branch_watch, u32 origin, u32 destination, u32 inst)
|
||||
{
|
||||
HitVirtualTrue_fk(branch_watch, FakeBranchWatchCollectionKey{origin, destination}, inst);
|
||||
}
|
||||
|
||||
static void HitPhysicalTrue(BranchWatch* branch_watch, u32 origin, u32 destination, u32 inst)
|
||||
{
|
||||
HitPhysicalTrue_fk(branch_watch, FakeBranchWatchCollectionKey{origin, destination}, inst);
|
||||
}
|
||||
|
||||
static void HitVirtualFalse(BranchWatch* branch_watch, u32 origin, u32 destination, u32 inst)
|
||||
{
|
||||
HitVirtualFalse_fk(branch_watch, FakeBranchWatchCollectionKey{origin, destination}, inst);
|
||||
}
|
||||
|
||||
static void HitPhysicalFalse(BranchWatch* branch_watch, u32 origin, u32 destination, u32 inst)
|
||||
{
|
||||
HitPhysicalFalse_fk(branch_watch, FakeBranchWatchCollectionKey{origin, destination}, inst);
|
||||
}
|
||||
|
||||
void HitTrue(u32 origin, u32 destination, UGeckoInstruction inst, bool translate)
|
||||
{
|
||||
if (translate)
|
||||
HitVirtualTrue(this, origin, destination, inst.hex);
|
||||
else
|
||||
HitPhysicalTrue(this, origin, destination, inst.hex);
|
||||
}
|
||||
|
||||
void HitFalse(u32 origin, u32 destination, UGeckoInstruction inst, bool translate)
|
||||
{
|
||||
if (translate)
|
||||
HitVirtualFalse(this, origin, destination, inst.hex);
|
||||
else
|
||||
HitPhysicalFalse(this, origin, destination, inst.hex);
|
||||
}
|
||||
|
||||
// The JIT needs this value, but doesn't need to be a full-on friend.
|
||||
static constexpr int GetOffsetOfRecordingActive()
|
||||
{
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Winvalid-offsetof"
|
||||
#endif
|
||||
return offsetof(BranchWatch, m_recording_active);
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
Collection& GetCollectionV(bool condition)
|
||||
{
|
||||
if (condition)
|
||||
return m_collection_vt;
|
||||
return m_collection_vf;
|
||||
}
|
||||
|
||||
Collection& GetCollectionP(bool condition)
|
||||
{
|
||||
if (condition)
|
||||
return m_collection_pt;
|
||||
return m_collection_pf;
|
||||
}
|
||||
|
||||
Collection& GetCollection(bool is_virtual, bool condition)
|
||||
{
|
||||
if (is_virtual)
|
||||
return GetCollectionV(condition);
|
||||
return GetCollectionP(condition);
|
||||
}
|
||||
|
||||
std::size_t m_blacklist_size = 0;
|
||||
Phase m_recording_phase = Phase::Blacklist;
|
||||
bool m_recording_active = false;
|
||||
Collection m_collection_vt; // virtual address space | true path
|
||||
Collection m_collection_vf; // virtual address space | false path
|
||||
Collection m_collection_pt; // physical address space | true path
|
||||
Collection m_collection_pf; // physical address space | false path
|
||||
Selection m_selection;
|
||||
};
|
||||
|
||||
#if _M_X86_64
|
||||
static_assert(BranchWatch::GetOffsetOfRecordingActive() < 0x80); // Makes JIT code smaller.
|
||||
#endif
|
||||
} // namespace Core
|
|
@ -64,8 +64,8 @@ std::optional<IPCReply> AesDevice::IOCtlV(const IOCtlVRequest& request)
|
|||
|
||||
std::vector<u8> input = std::vector<u8>(request.in_vectors[0].size);
|
||||
std::vector<u8> output = std::vector<u8>(request.io_vectors[0].size);
|
||||
std::array<u8, 10> key = {0};
|
||||
std::array<u8, 10> iv = {0};
|
||||
std::array<u8, 0x10> key = {0};
|
||||
std::array<u8, 0x10> iv = {0};
|
||||
memory.CopyFromEmu(input.data(), request.in_vectors[0].address, input.size());
|
||||
memory.CopyFromEmu(key.data(), request.in_vectors[1].address, key.size());
|
||||
memory.CopyFromEmu(iv.data(), request.io_vectors[1].address, iv.size());
|
||||
|
|
|
@ -46,7 +46,7 @@ HostFileSystem::HostFilename HostFileSystem::BuildFilename(const std::string& wi
|
|||
if (wii_path.compare(0, 1, "/") == 0)
|
||||
return HostFilename{m_root_path + Common::EscapePath(wii_path), false};
|
||||
|
||||
ASSERT(false);
|
||||
ASSERT_MSG(IOS_FS, false, "Invalid Wii path '{}' given to BuildFilename()", wii_path);
|
||||
return HostFilename{m_root_path, false};
|
||||
}
|
||||
|
||||
|
|
|
@ -64,8 +64,9 @@ void Interpreter::UpdatePC()
|
|||
m_ppc_state.pc = m_ppc_state.npc;
|
||||
}
|
||||
|
||||
Interpreter::Interpreter(Core::System& system, PowerPC::PowerPCState& ppc_state, PowerPC::MMU& mmu)
|
||||
: m_system(system), m_ppc_state(ppc_state), m_mmu(mmu)
|
||||
Interpreter::Interpreter(Core::System& system, PowerPC::PowerPCState& ppc_state, PowerPC::MMU& mmu,
|
||||
Core::BranchWatch& branch_watch)
|
||||
: m_system(system), m_ppc_state(ppc_state), m_mmu(mmu), m_branch_watch(branch_watch)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,9 @@
|
|||
|
||||
namespace Core
|
||||
{
|
||||
class BranchWatch;
|
||||
class System;
|
||||
}
|
||||
} // namespace Core
|
||||
namespace PowerPC
|
||||
{
|
||||
class MMU;
|
||||
|
@ -22,7 +23,8 @@ struct PowerPCState;
|
|||
class Interpreter : public CPUCoreBase
|
||||
{
|
||||
public:
|
||||
Interpreter(Core::System& system, PowerPC::PowerPCState& ppc_state, PowerPC::MMU& mmu);
|
||||
Interpreter(Core::System& system, PowerPC::PowerPCState& ppc_state, PowerPC::MMU& mmu,
|
||||
Core::BranchWatch& branch_watch);
|
||||
Interpreter(const Interpreter&) = delete;
|
||||
Interpreter(Interpreter&&) = delete;
|
||||
Interpreter& operator=(const Interpreter&) = delete;
|
||||
|
@ -314,6 +316,7 @@ private:
|
|||
Core::System& m_system;
|
||||
PowerPC::PowerPCState& m_ppc_state;
|
||||
PowerPC::MMU& m_mmu;
|
||||
Core::BranchWatch& m_branch_watch;
|
||||
|
||||
UGeckoInstruction m_prev_inst{};
|
||||
u32 m_last_pc = 0;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "Common/CommonTypes.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
#include "Core/PowerPC/Interpreter/ExceptionUtils.h"
|
||||
#include "Core/PowerPC/PowerPC.h"
|
||||
|
@ -19,12 +20,13 @@ void Interpreter::bx(Interpreter& interpreter, UGeckoInstruction inst)
|
|||
if (inst.LK)
|
||||
LR(ppc_state) = ppc_state.pc + 4;
|
||||
|
||||
const auto address = u32(SignExt26(inst.LI << 2));
|
||||
u32 destination_addr = u32(SignExt26(inst.LI << 2));
|
||||
if (!inst.AA)
|
||||
destination_addr += ppc_state.pc;
|
||||
ppc_state.npc = destination_addr;
|
||||
|
||||
if (inst.AA)
|
||||
ppc_state.npc = address;
|
||||
else
|
||||
ppc_state.npc = ppc_state.pc + address;
|
||||
if (auto& branch_watch = interpreter.m_branch_watch; branch_watch.GetRecordingActive())
|
||||
branch_watch.HitTrue(ppc_state.pc, destination_addr, inst, ppc_state.msr.IR);
|
||||
|
||||
interpreter.m_end_block = true;
|
||||
}
|
||||
|
@ -33,6 +35,7 @@ void Interpreter::bx(Interpreter& interpreter, UGeckoInstruction inst)
|
|||
void Interpreter::bcx(Interpreter& interpreter, UGeckoInstruction inst)
|
||||
{
|
||||
auto& ppc_state = interpreter.m_ppc_state;
|
||||
auto& branch_watch = interpreter.m_branch_watch;
|
||||
|
||||
if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0)
|
||||
CTR(ppc_state)--;
|
||||
|
@ -49,12 +52,17 @@ void Interpreter::bcx(Interpreter& interpreter, UGeckoInstruction inst)
|
|||
if (inst.LK)
|
||||
LR(ppc_state) = ppc_state.pc + 4;
|
||||
|
||||
const auto address = u32(SignExt16(s16(inst.BD << 2)));
|
||||
u32 destination_addr = u32(SignExt16(s16(inst.BD << 2)));
|
||||
if (!inst.AA)
|
||||
destination_addr += ppc_state.pc;
|
||||
ppc_state.npc = destination_addr;
|
||||
|
||||
if (inst.AA)
|
||||
ppc_state.npc = address;
|
||||
else
|
||||
ppc_state.npc = ppc_state.pc + address;
|
||||
if (branch_watch.GetRecordingActive())
|
||||
branch_watch.HitTrue(ppc_state.pc, destination_addr, inst, ppc_state.msr.IR);
|
||||
}
|
||||
else if (branch_watch.GetRecordingActive())
|
||||
{
|
||||
branch_watch.HitFalse(ppc_state.pc, ppc_state.pc + 4, inst, ppc_state.msr.IR);
|
||||
}
|
||||
|
||||
interpreter.m_end_block = true;
|
||||
|
@ -63,6 +71,7 @@ void Interpreter::bcx(Interpreter& interpreter, UGeckoInstruction inst)
|
|||
void Interpreter::bcctrx(Interpreter& interpreter, UGeckoInstruction inst)
|
||||
{
|
||||
auto& ppc_state = interpreter.m_ppc_state;
|
||||
auto& branch_watch = interpreter.m_branch_watch;
|
||||
|
||||
DEBUG_ASSERT_MSG(POWERPC, (inst.BO_2 & BO_DONT_DECREMENT_FLAG) != 0,
|
||||
"bcctrx with decrement and test CTR option is invalid!");
|
||||
|
@ -72,9 +81,17 @@ void Interpreter::bcctrx(Interpreter& interpreter, UGeckoInstruction inst)
|
|||
|
||||
if (condition != 0)
|
||||
{
|
||||
ppc_state.npc = CTR(ppc_state) & (~3);
|
||||
const u32 destination_addr = CTR(ppc_state) & (~3);
|
||||
ppc_state.npc = destination_addr;
|
||||
if (inst.LK_3)
|
||||
LR(ppc_state) = ppc_state.pc + 4;
|
||||
|
||||
if (branch_watch.GetRecordingActive())
|
||||
branch_watch.HitTrue(ppc_state.pc, destination_addr, inst, ppc_state.msr.IR);
|
||||
}
|
||||
else if (branch_watch.GetRecordingActive())
|
||||
{
|
||||
branch_watch.HitFalse(ppc_state.pc, ppc_state.pc + 4, inst, ppc_state.msr.IR);
|
||||
}
|
||||
|
||||
interpreter.m_end_block = true;
|
||||
|
@ -83,6 +100,7 @@ void Interpreter::bcctrx(Interpreter& interpreter, UGeckoInstruction inst)
|
|||
void Interpreter::bclrx(Interpreter& interpreter, UGeckoInstruction inst)
|
||||
{
|
||||
auto& ppc_state = interpreter.m_ppc_state;
|
||||
auto& branch_watch = interpreter.m_branch_watch;
|
||||
|
||||
if ((inst.BO_2 & BO_DONT_DECREMENT_FLAG) == 0)
|
||||
CTR(ppc_state)--;
|
||||
|
@ -93,9 +111,17 @@ void Interpreter::bclrx(Interpreter& interpreter, UGeckoInstruction inst)
|
|||
|
||||
if ((counter & condition) != 0)
|
||||
{
|
||||
ppc_state.npc = LR(ppc_state) & (~3);
|
||||
const u32 destination_addr = LR(ppc_state) & (~3);
|
||||
ppc_state.npc = destination_addr;
|
||||
if (inst.LK_3)
|
||||
LR(ppc_state) = ppc_state.pc + 4;
|
||||
|
||||
if (branch_watch.GetRecordingActive())
|
||||
branch_watch.HitTrue(ppc_state.pc, destination_addr, inst, ppc_state.msr.IR);
|
||||
}
|
||||
else if (branch_watch.GetRecordingActive())
|
||||
{
|
||||
branch_watch.HitFalse(ppc_state.pc, ppc_state.pc + 4, inst, ppc_state.msr.IR);
|
||||
}
|
||||
|
||||
interpreter.m_end_block = true;
|
||||
|
|
|
@ -1041,7 +1041,18 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC)
|
|||
if (HandleFunctionHooking(op.address))
|
||||
break;
|
||||
|
||||
if (!op.skip)
|
||||
if (op.skip)
|
||||
{
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// The only thing that currently sets op.skip is the BLR following optimization.
|
||||
// If any non-branch instruction starts setting that too, this will need to be changed.
|
||||
ASSERT(op.inst.hex == 0x4e800020);
|
||||
WriteBranchWatch<true>(op.address, op.branchTo, op.inst, RSCRATCH, RSCRATCH2,
|
||||
CallerSavedRegistersInUse());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((opinfo->flags & FL_USE_FPU) && !js.firstFPInstructionFound)
|
||||
{
|
||||
|
|
|
@ -98,6 +98,12 @@ public:
|
|||
void WriteExternalExceptionExit();
|
||||
void WriteRfiExitDestInRSCRATCH();
|
||||
void WriteIdleExit(u32 destination);
|
||||
template <bool condition>
|
||||
void WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, Gen::X64Reg reg_a,
|
||||
Gen::X64Reg reg_b, BitSet32 caller_save);
|
||||
void WriteBranchWatchDestInRSCRATCH(u32 origin, UGeckoInstruction inst, Gen::X64Reg reg_a,
|
||||
Gen::X64Reg reg_b, BitSet32 caller_save);
|
||||
|
||||
bool Cleanup();
|
||||
|
||||
void GenerateConstantOverflow(bool overflow);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "Common/CommonTypes.h"
|
||||
#include "Common/x64Emitter.h"
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
#include "Core/PowerPC/Gekko.h"
|
||||
#include "Core/PowerPC/Jit64/RegCache/JitRegCache.h"
|
||||
#include "Core/PowerPC/Jit64Common/Jit64PowerPCState.h"
|
||||
|
@ -66,6 +67,68 @@ void Jit64::rfi(UGeckoInstruction inst)
|
|||
WriteRfiExitDestInRSCRATCH();
|
||||
}
|
||||
|
||||
template <bool condition>
|
||||
void Jit64::WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, X64Reg reg_a,
|
||||
X64Reg reg_b, BitSet32 caller_save)
|
||||
{
|
||||
MOV(64, R(reg_a), ImmPtr(&m_branch_watch));
|
||||
MOVZX(32, 8, reg_b, MDisp(reg_a, Core::BranchWatch::GetOffsetOfRecordingActive()));
|
||||
TEST(32, R(reg_b), R(reg_b));
|
||||
|
||||
FixupBranch branch_in = J_CC(CC_NZ, Jump::Near);
|
||||
SwitchToFarCode();
|
||||
SetJumpTarget(branch_in);
|
||||
|
||||
ABI_PushRegistersAndAdjustStack(caller_save, 0);
|
||||
// Some call sites have an optimization to use ABI_PARAM1 as a scratch register.
|
||||
if (reg_a != ABI_PARAM1)
|
||||
MOV(64, R(ABI_PARAM1), R(reg_a));
|
||||
MOV(64, R(ABI_PARAM2), Imm64(Core::FakeBranchWatchCollectionKey{origin, destination}));
|
||||
MOV(32, R(ABI_PARAM3), Imm32(inst.hex));
|
||||
ABI_CallFunction(m_ppc_state.msr.IR ? (condition ? &Core::BranchWatch::HitVirtualTrue_fk :
|
||||
&Core::BranchWatch::HitVirtualFalse_fk) :
|
||||
(condition ? &Core::BranchWatch::HitPhysicalTrue_fk :
|
||||
&Core::BranchWatch::HitPhysicalFalse_fk));
|
||||
ABI_PopRegistersAndAdjustStack(caller_save, 0);
|
||||
|
||||
FixupBranch branch_out = J(Jump::Near);
|
||||
SwitchToNearCode();
|
||||
SetJumpTarget(branch_out);
|
||||
}
|
||||
|
||||
template void Jit64::WriteBranchWatch<true>(u32, u32, UGeckoInstruction, X64Reg, X64Reg, BitSet32);
|
||||
template void Jit64::WriteBranchWatch<false>(u32, u32, UGeckoInstruction, X64Reg, X64Reg, BitSet32);
|
||||
|
||||
void Jit64::WriteBranchWatchDestInRSCRATCH(u32 origin, UGeckoInstruction inst, X64Reg reg_a,
|
||||
X64Reg reg_b, BitSet32 caller_save)
|
||||
{
|
||||
MOV(64, R(reg_a), ImmPtr(&m_branch_watch));
|
||||
MOVZX(32, 8, reg_b, MDisp(reg_a, Core::BranchWatch::GetOffsetOfRecordingActive()));
|
||||
TEST(32, R(reg_b), R(reg_b));
|
||||
|
||||
FixupBranch branch_in = J_CC(CC_NZ, Jump::Near);
|
||||
SwitchToFarCode();
|
||||
SetJumpTarget(branch_in);
|
||||
|
||||
// Assert RSCRATCH won't be clobbered before it is moved from.
|
||||
static_assert(ABI_PARAM1 != RSCRATCH);
|
||||
|
||||
ABI_PushRegistersAndAdjustStack(caller_save, 0);
|
||||
// Some call sites have an optimization to use ABI_PARAM1 as a scratch register.
|
||||
if (reg_a != ABI_PARAM1)
|
||||
MOV(64, R(ABI_PARAM1), R(reg_a));
|
||||
MOV(32, R(ABI_PARAM3), R(RSCRATCH));
|
||||
MOV(32, R(ABI_PARAM2), Imm32(origin));
|
||||
MOV(32, R(ABI_PARAM4), Imm32(inst.hex));
|
||||
ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue :
|
||||
&Core::BranchWatch::HitPhysicalTrue);
|
||||
ABI_PopRegistersAndAdjustStack(caller_save, 0);
|
||||
|
||||
FixupBranch branch_out = J(Jump::Near);
|
||||
SwitchToNearCode();
|
||||
SetJumpTarget(branch_out);
|
||||
}
|
||||
|
||||
void Jit64::bx(UGeckoInstruction inst)
|
||||
{
|
||||
INSTRUCTION_START
|
||||
|
@ -81,6 +144,11 @@ void Jit64::bx(UGeckoInstruction inst)
|
|||
// Because PPCAnalyst::Flatten() merged the blocks.
|
||||
if (!js.isLastInstruction)
|
||||
{
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, RSCRATCH, RSCRATCH2,
|
||||
CallerSavedRegistersInUse());
|
||||
}
|
||||
if (inst.LK && !js.op->skipLRStack)
|
||||
{
|
||||
// We have to fake the stack as the RET instruction was not
|
||||
|
@ -94,6 +162,11 @@ void Jit64::bx(UGeckoInstruction inst)
|
|||
gpr.Flush();
|
||||
fpr.Flush();
|
||||
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
#ifdef ACID_TEST
|
||||
if (inst.LK)
|
||||
AND(32, PPCSTATE(cr), Imm32(~(0xFF000000)));
|
||||
|
@ -144,6 +217,11 @@ void Jit64::bcx(UGeckoInstruction inst)
|
|||
if (!js.isLastInstruction && (inst.BO & BO_DONT_DECREMENT_FLAG) &&
|
||||
(inst.BO & BO_DONT_CHECK_CONDITION))
|
||||
{
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, RSCRATCH, RSCRATCH2,
|
||||
CallerSavedRegistersInUse());
|
||||
}
|
||||
if (inst.LK && !js.op->skipLRStack)
|
||||
{
|
||||
// We have to fake the stack as the RET instruction was not
|
||||
|
@ -160,6 +238,11 @@ void Jit64::bcx(UGeckoInstruction inst)
|
|||
gpr.Flush();
|
||||
fpr.Flush();
|
||||
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
if (js.op->branchIsIdleLoop)
|
||||
{
|
||||
WriteIdleExit(js.op->branchTo);
|
||||
|
@ -179,8 +262,18 @@ void Jit64::bcx(UGeckoInstruction inst)
|
|||
{
|
||||
gpr.Flush();
|
||||
fpr.Flush();
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
WriteExit(js.compilerPC + 4);
|
||||
}
|
||||
else if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2,
|
||||
CallerSavedRegistersInUse());
|
||||
}
|
||||
}
|
||||
|
||||
void Jit64::bcctrx(UGeckoInstruction inst)
|
||||
|
@ -204,6 +297,12 @@ void Jit64::bcctrx(UGeckoInstruction inst)
|
|||
if (inst.LK_3)
|
||||
MOV(32, PPCSTATE_LR, Imm32(js.compilerPC + 4)); // LR = PC + 4;
|
||||
AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC));
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2,
|
||||
BitSet32{RSCRATCH});
|
||||
}
|
||||
WriteExitDestInRSCRATCH(inst.LK_3, js.compilerPC + 4);
|
||||
}
|
||||
else
|
||||
|
@ -226,6 +325,12 @@ void Jit64::bcctrx(UGeckoInstruction inst)
|
|||
RCForkGuard fpr_guard = fpr.Fork();
|
||||
gpr.Flush();
|
||||
fpr.Flush();
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2,
|
||||
BitSet32{RSCRATCH});
|
||||
}
|
||||
WriteExitDestInRSCRATCH(inst.LK_3, js.compilerPC + 4);
|
||||
// Would really like to continue the block here, but it ends. TODO.
|
||||
}
|
||||
|
@ -235,8 +340,18 @@ void Jit64::bcctrx(UGeckoInstruction inst)
|
|||
{
|
||||
gpr.Flush();
|
||||
fpr.Flush();
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
WriteExit(js.compilerPC + 4);
|
||||
}
|
||||
else if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2,
|
||||
CallerSavedRegistersInUse());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,10 +385,8 @@ void Jit64::bclrx(UGeckoInstruction inst)
|
|||
|
||||
MOV(32, R(RSCRATCH), PPCSTATE_LR);
|
||||
// We don't have to do this because WriteBLRExit handles it for us. Specifically, since we only
|
||||
// ever push
|
||||
// divisible-by-four instruction addresses onto the stack, if the return address matches, we're
|
||||
// already
|
||||
// good. If it doesn't match, the mispredicted-BLR code handles the fixup.
|
||||
// ever push divisible-by-four instruction addresses onto the stack, if the return address
|
||||
// matches, we're already good. If it doesn't match, the mispredicted-BLR code handles the fixup.
|
||||
if (!m_enable_blr_optimization)
|
||||
AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC));
|
||||
if (inst.LK)
|
||||
|
@ -287,10 +400,21 @@ void Jit64::bclrx(UGeckoInstruction inst)
|
|||
|
||||
if (js.op->branchIsIdleLoop)
|
||||
{
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
WriteIdleExit(js.op->branchTo);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2,
|
||||
BitSet32{RSCRATCH});
|
||||
}
|
||||
WriteBLRExit();
|
||||
}
|
||||
}
|
||||
|
@ -304,6 +428,16 @@ void Jit64::bclrx(UGeckoInstruction inst)
|
|||
{
|
||||
gpr.Flush();
|
||||
fpr.Flush();
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
WriteExit(js.compilerPC + 4);
|
||||
}
|
||||
else if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2,
|
||||
CallerSavedRegistersInUse());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -394,18 +394,25 @@ void Jit64::DoMergedBranch()
|
|||
if (next.LK)
|
||||
MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4));
|
||||
|
||||
WriteIdleExit(js.op[1].branchTo);
|
||||
const u32 destination = js.op[1].branchTo;
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<true>(nextPC, destination, next, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
WriteIdleExit(destination);
|
||||
}
|
||||
else if (next.OPCD == 16) // bcx
|
||||
{
|
||||
if (next.LK)
|
||||
MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4));
|
||||
|
||||
u32 destination;
|
||||
if (next.AA)
|
||||
destination = SignExt16(next.BD << 2);
|
||||
else
|
||||
destination = nextPC + SignExt16(next.BD << 2);
|
||||
const u32 destination = js.op[1].branchTo;
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<true>(nextPC, destination, next, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
WriteExit(destination, next.LK, nextPC + 4);
|
||||
}
|
||||
else if ((next.OPCD == 19) && (next.SUBOP10 == 528)) // bcctrx
|
||||
|
@ -414,6 +421,11 @@ void Jit64::DoMergedBranch()
|
|||
MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4));
|
||||
MOV(32, R(RSCRATCH), PPCSTATE_SPR(SPR_CTR));
|
||||
AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC));
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatchDestInRSCRATCH(nextPC, next, ABI_PARAM1, RSCRATCH2, BitSet32{RSCRATCH});
|
||||
}
|
||||
WriteExitDestInRSCRATCH(next.LK, nextPC + 4);
|
||||
}
|
||||
else if ((next.OPCD == 19) && (next.SUBOP10 == 16)) // bclrx
|
||||
|
@ -423,6 +435,11 @@ void Jit64::DoMergedBranch()
|
|||
AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC));
|
||||
if (next.LK)
|
||||
MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4));
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatchDestInRSCRATCH(nextPC, next, ABI_PARAM1, RSCRATCH2, BitSet32{RSCRATCH});
|
||||
}
|
||||
WriteBLRExit();
|
||||
}
|
||||
else
|
||||
|
@ -480,8 +497,18 @@ void Jit64::DoMergedBranchCondition()
|
|||
{
|
||||
gpr.Flush();
|
||||
fpr.Flush();
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<false>(nextPC, nextPC + 4, next, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
WriteExit(nextPC + 4);
|
||||
}
|
||||
else if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<false>(nextPC, nextPC + 4, next, RSCRATCH, RSCRATCH2,
|
||||
CallerSavedRegistersInUse());
|
||||
}
|
||||
}
|
||||
|
||||
void Jit64::DoMergedBranchImmediate(s64 val)
|
||||
|
@ -515,8 +542,18 @@ void Jit64::DoMergedBranchImmediate(s64 val)
|
|||
{
|
||||
gpr.Flush();
|
||||
fpr.Flush();
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
|
||||
WriteBranchWatch<false>(nextPC, nextPC + 4, next, ABI_PARAM1, RSCRATCH, {});
|
||||
}
|
||||
WriteExit(nextPC + 4);
|
||||
}
|
||||
else if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<false>(nextPC, nextPC + 4, next, RSCRATCH, RSCRATCH2,
|
||||
CallerSavedRegistersInUse());
|
||||
}
|
||||
}
|
||||
|
||||
void Jit64::cmpXX(UGeckoInstruction inst)
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
#include "Core/HW/CPU.h"
|
||||
#include "Core/HW/Memmap.h"
|
||||
#include "Core/PowerPC/Jit64/RegCache/JitRegCache.h"
|
||||
|
@ -300,6 +301,40 @@ void Jit64::dcbx(UGeckoInstruction inst)
|
|||
|
||||
// Load the loop_counter register with the amount of invalidations to execute.
|
||||
LEA(32, loop_counter, MDisp(RSCRATCH2, 1));
|
||||
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
const X64Reg bw_reg_a = reg_cycle_count, bw_reg_b = reg_downcount;
|
||||
const BitSet32 bw_caller_save = (CallerSavedRegistersInUse() | BitSet32{RSCRATCH2}) &
|
||||
~BitSet32{int(bw_reg_a), int(bw_reg_b)};
|
||||
|
||||
MOV(64, R(bw_reg_a), ImmPtr(&m_branch_watch));
|
||||
MOVZX(32, 8, bw_reg_b, MDisp(bw_reg_a, Core::BranchWatch::GetOffsetOfRecordingActive()));
|
||||
TEST(32, R(bw_reg_b), R(bw_reg_b));
|
||||
|
||||
FixupBranch branch_in = J_CC(CC_NZ, Jump::Near);
|
||||
SwitchToFarCode();
|
||||
SetJumpTarget(branch_in);
|
||||
|
||||
// Assert RSCRATCH2 won't be clobbered before it is moved from.
|
||||
static_assert(RSCRATCH2 != ABI_PARAM1);
|
||||
|
||||
ABI_PushRegistersAndAdjustStack(bw_caller_save, 0);
|
||||
MOV(64, R(ABI_PARAM1), R(bw_reg_a));
|
||||
// RSCRATCH2 holds the amount of faked branch watch hits. Move RSCRATCH2 first, because
|
||||
// ABI_PARAM2 clobbers RSCRATCH2 on Windows and ABI_PARAM3 clobbers RSCRATCH2 on Linux!
|
||||
MOV(32, R(ABI_PARAM4), R(RSCRATCH2));
|
||||
const PPCAnalyst::CodeOp& op = js.op[2];
|
||||
MOV(64, R(ABI_PARAM2), Imm64(Core::FakeBranchWatchCollectionKey{op.address, op.branchTo}));
|
||||
MOV(32, R(ABI_PARAM3), Imm32(op.inst.hex));
|
||||
ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue_fk_n :
|
||||
&Core::BranchWatch::HitPhysicalTrue_fk_n);
|
||||
ABI_PopRegistersAndAdjustStack(bw_caller_save, 0);
|
||||
|
||||
FixupBranch branch_out = J(Jump::Near);
|
||||
SwitchToNearCode();
|
||||
SetJumpTarget(branch_out);
|
||||
}
|
||||
}
|
||||
|
||||
X64Reg addr = RSCRATCH;
|
||||
|
|
|
@ -1181,7 +1181,22 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC)
|
|||
if (HandleFunctionHooking(op.address))
|
||||
break;
|
||||
|
||||
if (!op.skip)
|
||||
if (op.skip)
|
||||
{
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
// The only thing that currently sets op.skip is the BLR following optimization.
|
||||
// If any non-branch instruction starts setting that too, this will need to be changed.
|
||||
ASSERT(op.inst.hex == 0x4e800020);
|
||||
const ARM64Reg bw_reg_a = gpr.GetReg(), bw_reg_b = gpr.GetReg();
|
||||
const BitSet32 gpr_caller_save =
|
||||
gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(bw_reg_a), DecodeReg(bw_reg_b)};
|
||||
WriteBranchWatch<true>(op.address, op.branchTo, op.inst, bw_reg_a, bw_reg_b,
|
||||
gpr_caller_save, fpr.GetCallerSavedUsed());
|
||||
gpr.Unlock(bw_reg_a, bw_reg_b);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((opinfo->flags & FL_USE_FPU) && !js.firstFPInstructionFound)
|
||||
{
|
||||
|
|
|
@ -315,6 +315,16 @@ protected:
|
|||
void MSRUpdated(u32 msr);
|
||||
void MSRUpdated(Arm64Gen::ARM64Reg msr);
|
||||
|
||||
// Branch Watch
|
||||
template <bool condition>
|
||||
void WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst,
|
||||
Arm64Gen::ARM64Reg reg_a, Arm64Gen::ARM64Reg reg_b,
|
||||
BitSet32 gpr_caller_save, BitSet32 fpr_caller_save);
|
||||
void WriteBranchWatchDestInRegister(u32 origin, Arm64Gen::ARM64Reg destination,
|
||||
UGeckoInstruction inst, Arm64Gen::ARM64Reg reg_a,
|
||||
Arm64Gen::ARM64Reg reg_b, BitSet32 gpr_caller_save,
|
||||
BitSet32 fpr_caller_save);
|
||||
|
||||
// Exits
|
||||
void
|
||||
WriteExit(u32 destination, bool LK = false, u32 exit_address_after_return = 0,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include "Core/Core.h"
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
#include "Core/PowerPC/JitArm64/JitArm64_RegCache.h"
|
||||
#include "Core/PowerPC/PPCTables.h"
|
||||
#include "Core/PowerPC/PowerPC.h"
|
||||
|
@ -74,6 +75,70 @@ void JitArm64::rfi(UGeckoInstruction inst)
|
|||
gpr.Unlock(WA);
|
||||
}
|
||||
|
||||
template <bool condition>
|
||||
void JitArm64::WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, ARM64Reg reg_a,
|
||||
ARM64Reg reg_b, BitSet32 gpr_caller_save, BitSet32 fpr_caller_save)
|
||||
{
|
||||
const ARM64Reg branch_watch = EncodeRegTo64(reg_a);
|
||||
MOVP2R(branch_watch, &m_branch_watch);
|
||||
LDRB(IndexType::Unsigned, reg_b, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive());
|
||||
FixupBranch branch_over = CBZ(reg_b);
|
||||
|
||||
FixupBranch branch_in = B();
|
||||
SwitchToFarCode();
|
||||
SetJumpTarget(branch_in);
|
||||
|
||||
const ARM64Reg float_emit_tmp = EncodeRegTo64(reg_b);
|
||||
ABI_PushRegisters(gpr_caller_save);
|
||||
m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp);
|
||||
ABI_CallFunction(m_ppc_state.msr.IR ? (condition ? &Core::BranchWatch::HitVirtualTrue_fk :
|
||||
&Core::BranchWatch::HitVirtualFalse_fk) :
|
||||
(condition ? &Core::BranchWatch::HitPhysicalTrue_fk :
|
||||
&Core::BranchWatch::HitPhysicalFalse_fk),
|
||||
branch_watch, Core::FakeBranchWatchCollectionKey{origin, destination}, inst.hex);
|
||||
m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp);
|
||||
ABI_PopRegisters(gpr_caller_save);
|
||||
|
||||
FixupBranch branch_out = B();
|
||||
SwitchToNearCode();
|
||||
SetJumpTarget(branch_out);
|
||||
SetJumpTarget(branch_over);
|
||||
}
|
||||
|
||||
template void JitArm64::WriteBranchWatch<true>(u32, u32, UGeckoInstruction, ARM64Reg, ARM64Reg,
|
||||
BitSet32, BitSet32);
|
||||
template void JitArm64::WriteBranchWatch<false>(u32, u32, UGeckoInstruction, ARM64Reg, ARM64Reg,
|
||||
BitSet32, BitSet32);
|
||||
|
||||
void JitArm64::WriteBranchWatchDestInRegister(u32 origin, ARM64Reg destination,
|
||||
UGeckoInstruction inst, ARM64Reg reg_a,
|
||||
ARM64Reg reg_b, BitSet32 gpr_caller_save,
|
||||
BitSet32 fpr_caller_save)
|
||||
{
|
||||
const ARM64Reg branch_watch = EncodeRegTo64(reg_a);
|
||||
MOVP2R(branch_watch, &m_branch_watch);
|
||||
LDRB(IndexType::Unsigned, reg_b, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive());
|
||||
FixupBranch branch_over = CBZ(reg_b);
|
||||
|
||||
FixupBranch branch_in = B();
|
||||
SwitchToFarCode();
|
||||
SetJumpTarget(branch_in);
|
||||
|
||||
const ARM64Reg float_emit_tmp = EncodeRegTo64(reg_b);
|
||||
ABI_PushRegisters(gpr_caller_save);
|
||||
m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp);
|
||||
ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue :
|
||||
&Core::BranchWatch::HitPhysicalTrue,
|
||||
branch_watch, origin, destination, inst.hex);
|
||||
m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp);
|
||||
ABI_PopRegisters(gpr_caller_save);
|
||||
|
||||
FixupBranch branch_out = B();
|
||||
SwitchToNearCode();
|
||||
SetJumpTarget(branch_out);
|
||||
SetJumpTarget(branch_over);
|
||||
}
|
||||
|
||||
void JitArm64::bx(UGeckoInstruction inst)
|
||||
{
|
||||
INSTRUCTION_START
|
||||
|
@ -89,6 +154,16 @@ void JitArm64::bx(UGeckoInstruction inst)
|
|||
|
||||
if (!js.isLastInstruction)
|
||||
{
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
const ARM64Reg WB = gpr.GetReg(), WC = gpr.GetReg();
|
||||
BitSet32 gpr_caller_save = gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WB), DecodeReg(WC)};
|
||||
if (WA != ARM64Reg::INVALID_REG && js.op->skipLRStack)
|
||||
gpr_caller_save[DecodeReg(WA)] = false;
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, WB, WC, gpr_caller_save,
|
||||
fpr.GetCallerSavedUsed());
|
||||
gpr.Unlock(WB, WC);
|
||||
}
|
||||
if (inst.LK && !js.op->skipLRStack)
|
||||
{
|
||||
// We have to fake the stack as the RET instruction was not
|
||||
|
@ -108,22 +183,37 @@ void JitArm64::bx(UGeckoInstruction inst)
|
|||
|
||||
if (js.op->branchIsIdleLoop)
|
||||
{
|
||||
if (WA != ARM64Reg::INVALID_REG)
|
||||
gpr.Unlock(WA);
|
||||
if (WA == ARM64Reg::INVALID_REG)
|
||||
WA = gpr.GetReg();
|
||||
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
const ARM64Reg WB = gpr.GetReg();
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, WA, WB, {}, {});
|
||||
gpr.Unlock(WB);
|
||||
}
|
||||
|
||||
// make idle loops go faster
|
||||
ARM64Reg WB = gpr.GetReg();
|
||||
ARM64Reg XB = EncodeRegTo64(WB);
|
||||
ARM64Reg XA = EncodeRegTo64(WA);
|
||||
|
||||
MOVP2R(XB, &CoreTiming::GlobalIdle);
|
||||
BLR(XB);
|
||||
gpr.Unlock(WB);
|
||||
MOVP2R(XA, &CoreTiming::GlobalIdle);
|
||||
BLR(XA);
|
||||
gpr.Unlock(WA);
|
||||
|
||||
WriteExceptionExit(js.op->branchTo);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, inst.LK ? WA : ARM64Reg::INVALID_REG);
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
const ARM64Reg WB = gpr.GetReg(), WC = gpr.GetReg();
|
||||
const BitSet32 gpr_caller_save =
|
||||
WA != ARM64Reg::INVALID_REG ? BitSet32{DecodeReg(WA)} & CALLER_SAVED_GPRS : BitSet32{};
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, WB, WC, gpr_caller_save, {});
|
||||
gpr.Unlock(WB, WC);
|
||||
}
|
||||
WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, WA);
|
||||
|
||||
if (WA != ARM64Reg::INVALID_REG)
|
||||
gpr.Unlock(WA);
|
||||
}
|
||||
|
@ -134,7 +224,9 @@ void JitArm64::bcx(UGeckoInstruction inst)
|
|||
JITDISABLE(bJITBranchOff);
|
||||
|
||||
ARM64Reg WA = gpr.GetReg();
|
||||
ARM64Reg WB = inst.LK ? gpr.GetReg() : WA;
|
||||
ARM64Reg WB = inst.LK || IsDebuggingEnabled() ? gpr.GetReg() : WA;
|
||||
ARM64Reg WC = IsDebuggingEnabled() && inst.LK && !js.op->branchIsIdleLoop ? gpr.GetReg() :
|
||||
ARM64Reg::INVALID_REG;
|
||||
|
||||
FixupBranch pCTRDontBranch;
|
||||
if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) // Decrement and test CTR
|
||||
|
@ -166,6 +258,19 @@ void JitArm64::bcx(UGeckoInstruction inst)
|
|||
gpr.Flush(FlushMode::MaintainState, WB);
|
||||
fpr.Flush(FlushMode::MaintainState, ARM64Reg::INVALID_REG);
|
||||
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
ARM64Reg bw_reg_a, bw_reg_b;
|
||||
// WC is only allocated when WA is needed for WriteExit and cannot be clobbered.
|
||||
if (WC == ARM64Reg::INVALID_REG)
|
||||
bw_reg_a = WA, bw_reg_b = WB;
|
||||
else
|
||||
bw_reg_a = WB, bw_reg_b = WC;
|
||||
const BitSet32 gpr_caller_save =
|
||||
gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(bw_reg_a), DecodeReg(bw_reg_b)};
|
||||
WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, bw_reg_a, bw_reg_b,
|
||||
gpr_caller_save, fpr.GetCallerSavedUsed());
|
||||
}
|
||||
if (js.op->branchIsIdleLoop)
|
||||
{
|
||||
// make idle loops go faster
|
||||
|
@ -178,7 +283,7 @@ void JitArm64::bcx(UGeckoInstruction inst)
|
|||
}
|
||||
else
|
||||
{
|
||||
WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, inst.LK ? WA : ARM64Reg::INVALID_REG);
|
||||
WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, WA);
|
||||
}
|
||||
|
||||
if ((inst.BO & BO_DONT_CHECK_CONDITION) == 0)
|
||||
|
@ -186,12 +291,26 @@ void JitArm64::bcx(UGeckoInstruction inst)
|
|||
if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0)
|
||||
SetJumpTarget(pCTRDontBranch);
|
||||
|
||||
if (WC != ARM64Reg::INVALID_REG)
|
||||
gpr.Unlock(WC);
|
||||
|
||||
if (!analyzer.HasOption(PPCAnalyst::PPCAnalyzer::OPTION_CONDITIONAL_CONTINUE))
|
||||
{
|
||||
gpr.Flush(FlushMode::All, WA);
|
||||
fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG);
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, {}, {});
|
||||
}
|
||||
WriteExit(js.compilerPC + 4);
|
||||
}
|
||||
else if (IsDebuggingEnabled())
|
||||
{
|
||||
const BitSet32 gpr_caller_save =
|
||||
gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WA), DecodeReg(WB)};
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, gpr_caller_save,
|
||||
fpr.GetCallerSavedUsed());
|
||||
}
|
||||
|
||||
gpr.Unlock(WA);
|
||||
if (WB != WA)
|
||||
|
@ -231,7 +350,17 @@ void JitArm64::bcctrx(UGeckoInstruction inst)
|
|||
LDR(IndexType::Unsigned, WA, PPC_REG, PPCSTATE_OFF_SPR(SPR_CTR));
|
||||
AND(WA, WA, LogicalImm(~0x3, GPRSize::B32));
|
||||
|
||||
WriteExit(WA, inst.LK_3, js.compilerPC + 4, inst.LK_3 ? WB : ARM64Reg::INVALID_REG);
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
const ARM64Reg WC = gpr.GetReg(), WD = gpr.GetReg();
|
||||
BitSet32 gpr_caller_save = BitSet32{DecodeReg(WA)};
|
||||
if (WB != ARM64Reg::INVALID_REG)
|
||||
gpr_caller_save[DecodeReg(WB)] = true;
|
||||
gpr_caller_save &= CALLER_SAVED_GPRS;
|
||||
WriteBranchWatchDestInRegister(js.compilerPC, WA, inst, WC, WD, gpr_caller_save, {});
|
||||
gpr.Unlock(WC, WD);
|
||||
}
|
||||
WriteExit(WA, inst.LK_3, js.compilerPC + 4, WB);
|
||||
|
||||
if (WB != ARM64Reg::INVALID_REG)
|
||||
gpr.Unlock(WB);
|
||||
|
@ -247,7 +376,9 @@ void JitArm64::bclrx(UGeckoInstruction inst)
|
|||
(inst.BO & BO_DONT_DECREMENT_FLAG) == 0 || (inst.BO & BO_DONT_CHECK_CONDITION) == 0;
|
||||
|
||||
ARM64Reg WA = gpr.GetReg();
|
||||
ARM64Reg WB = conditional || inst.LK ? gpr.GetReg() : ARM64Reg::INVALID_REG;
|
||||
ARM64Reg WB =
|
||||
conditional || inst.LK || IsDebuggingEnabled() ? gpr.GetReg() : ARM64Reg::INVALID_REG;
|
||||
ARM64Reg WC = IsDebuggingEnabled() ? gpr.GetReg() : ARM64Reg::INVALID_REG;
|
||||
|
||||
FixupBranch pCTRDontBranch;
|
||||
if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) // Decrement and test CTR
|
||||
|
@ -281,6 +412,26 @@ void JitArm64::bclrx(UGeckoInstruction inst)
|
|||
gpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::All, WB);
|
||||
fpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::All, ARM64Reg::INVALID_REG);
|
||||
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
BitSet32 gpr_caller_save;
|
||||
BitSet32 fpr_caller_save;
|
||||
if (conditional)
|
||||
{
|
||||
gpr_caller_save = gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WB), DecodeReg(WC)};
|
||||
if (js.op->branchIsIdleLoop)
|
||||
gpr_caller_save[DecodeReg(WA)] = false;
|
||||
fpr_caller_save = fpr.GetCallerSavedUsed();
|
||||
}
|
||||
else
|
||||
{
|
||||
gpr_caller_save =
|
||||
js.op->branchIsIdleLoop ? BitSet32{} : BitSet32{DecodeReg(WA)} & CALLER_SAVED_GPRS;
|
||||
fpr_caller_save = {};
|
||||
}
|
||||
WriteBranchWatchDestInRegister(js.compilerPC, WA, inst, WB, WC, gpr_caller_save,
|
||||
fpr_caller_save);
|
||||
}
|
||||
if (js.op->branchIsIdleLoop)
|
||||
{
|
||||
// make idle loops go faster
|
||||
|
@ -301,12 +452,26 @@ void JitArm64::bclrx(UGeckoInstruction inst)
|
|||
if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0)
|
||||
SetJumpTarget(pCTRDontBranch);
|
||||
|
||||
if (WC != ARM64Reg::INVALID_REG)
|
||||
gpr.Unlock(WC);
|
||||
|
||||
if (!analyzer.HasOption(PPCAnalyst::PPCAnalyzer::OPTION_CONDITIONAL_CONTINUE))
|
||||
{
|
||||
gpr.Flush(FlushMode::All, WA);
|
||||
fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG);
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, {}, {});
|
||||
}
|
||||
WriteExit(js.compilerPC + 4);
|
||||
}
|
||||
else if (IsDebuggingEnabled())
|
||||
{
|
||||
const BitSet32 gpr_caller_save =
|
||||
gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WA), DecodeReg(WB)};
|
||||
WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, gpr_caller_save,
|
||||
fpr.GetCallerSavedUsed());
|
||||
}
|
||||
|
||||
gpr.Unlock(WA);
|
||||
if (WB != ARM64Reg::INVALID_REG)
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
#include "Core/HW/DSP.h"
|
||||
#include "Core/HW/MMIO.h"
|
||||
#include "Core/HW/Memmap.h"
|
||||
|
@ -769,18 +770,15 @@ void JitArm64::dcbx(UGeckoInstruction inst)
|
|||
js.op[1].inst.RA_6 == b && js.op[1].inst.RD_2 == b &&
|
||||
js.op[2].inst.hex == 0x4200fff8;
|
||||
|
||||
gpr.Lock(ARM64Reg::W0, ARM64Reg::W1);
|
||||
if (make_loop)
|
||||
gpr.Lock(ARM64Reg::W2);
|
||||
constexpr ARM64Reg WA = ARM64Reg::W0, WB = ARM64Reg::W1, loop_counter = ARM64Reg::W2;
|
||||
// Be careful, loop_counter is only locked when make_loop == true.
|
||||
gpr.Lock(WA, WB);
|
||||
|
||||
ARM64Reg WA = ARM64Reg::W0;
|
||||
|
||||
if (make_loop)
|
||||
gpr.BindToRegister(b, true);
|
||||
|
||||
ARM64Reg loop_counter = ARM64Reg::INVALID_REG;
|
||||
if (make_loop)
|
||||
{
|
||||
gpr.Lock(loop_counter);
|
||||
gpr.BindToRegister(b, true);
|
||||
|
||||
// We'll execute somewhere between one single cacheline invalidation and however many are needed
|
||||
// to reduce the downcount to zero, never exceeding the amount requested by the game.
|
||||
// To stay consistent with the rest of the code we adjust the involved registers (CTR and Rb)
|
||||
|
@ -788,10 +786,8 @@ void JitArm64::dcbx(UGeckoInstruction inst)
|
|||
// bdnz afterwards! So if we invalidate a single cache line, we don't adjust the registers at
|
||||
// all, if we invalidate 2 cachelines we adjust the registers by one step, and so on.
|
||||
|
||||
ARM64Reg reg_cycle_count = gpr.GetReg();
|
||||
ARM64Reg reg_downcount = gpr.GetReg();
|
||||
loop_counter = ARM64Reg::W2;
|
||||
ARM64Reg WB = ARM64Reg::W1;
|
||||
const ARM64Reg reg_cycle_count = gpr.GetReg();
|
||||
const ARM64Reg reg_downcount = gpr.GetReg();
|
||||
|
||||
// Figure out how many loops we want to do.
|
||||
const u8 cycle_count_per_loop =
|
||||
|
@ -828,11 +824,43 @@ void JitArm64::dcbx(UGeckoInstruction inst)
|
|||
// Load the loop_counter register with the amount of invalidations to execute.
|
||||
ADD(loop_counter, WA, 1);
|
||||
|
||||
if (IsDebuggingEnabled())
|
||||
{
|
||||
const ARM64Reg branch_watch = EncodeRegTo64(reg_cycle_count);
|
||||
MOVP2R(branch_watch, &m_branch_watch);
|
||||
LDRB(IndexType::Unsigned, WB, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive());
|
||||
FixupBranch branch_over = CBZ(WB);
|
||||
|
||||
FixupBranch branch_in = B();
|
||||
SwitchToFarCode();
|
||||
SetJumpTarget(branch_in);
|
||||
|
||||
const BitSet32 gpr_caller_save =
|
||||
gpr.GetCallerSavedUsed() &
|
||||
~BitSet32{DecodeReg(WB), DecodeReg(reg_cycle_count), DecodeReg(reg_downcount)};
|
||||
ABI_PushRegisters(gpr_caller_save);
|
||||
const ARM64Reg float_emit_tmp = EncodeRegTo64(WB);
|
||||
const BitSet32 fpr_caller_save = fpr.GetCallerSavedUsed();
|
||||
m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp);
|
||||
const PPCAnalyst::CodeOp& op = js.op[2];
|
||||
ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue_fk_n :
|
||||
&Core::BranchWatch::HitPhysicalTrue_fk_n,
|
||||
branch_watch, Core::FakeBranchWatchCollectionKey{op.address, op.branchTo},
|
||||
op.inst.hex, WA);
|
||||
m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp);
|
||||
ABI_PopRegisters(gpr_caller_save);
|
||||
|
||||
FixupBranch branch_out = B();
|
||||
SwitchToNearCode();
|
||||
SetJumpTarget(branch_out);
|
||||
SetJumpTarget(branch_over);
|
||||
}
|
||||
|
||||
gpr.Unlock(reg_cycle_count, reg_downcount);
|
||||
}
|
||||
|
||||
ARM64Reg effective_addr = ARM64Reg::W1;
|
||||
ARM64Reg physical_addr = gpr.GetReg();
|
||||
constexpr ARM64Reg effective_addr = WB;
|
||||
const ARM64Reg physical_addr = gpr.GetReg();
|
||||
|
||||
if (a)
|
||||
ADD(effective_addr, gpr.R(a), gpr.R(b));
|
||||
|
@ -911,7 +939,7 @@ void JitArm64::dcbx(UGeckoInstruction inst)
|
|||
SwitchToNearCode();
|
||||
SetJumpTarget(near_addr);
|
||||
|
||||
gpr.Unlock(effective_addr, physical_addr, WA);
|
||||
gpr.Unlock(WA, WB, physical_addr);
|
||||
if (make_loop)
|
||||
gpr.Unlock(loop_counter);
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ void JitTrampoline(JitBase& jit, u32 em_address)
|
|||
|
||||
JitBase::JitBase(Core::System& system)
|
||||
: m_code_buffer(code_buffer_size), m_system(system), m_ppc_state(system.GetPPCState()),
|
||||
m_mmu(system.GetMMU())
|
||||
m_mmu(system.GetMMU()), m_branch_watch(system.GetPowerPC().GetBranchWatch())
|
||||
{
|
||||
m_registered_config_callback_id = CPUThreadConfigCallback::AddConfigChangedCallback([this] {
|
||||
if (DoesConfigNeedRefresh())
|
||||
|
|
|
@ -23,8 +23,9 @@
|
|||
|
||||
namespace Core
|
||||
{
|
||||
class BranchWatch;
|
||||
class System;
|
||||
}
|
||||
} // namespace Core
|
||||
namespace PowerPC
|
||||
{
|
||||
class MMU;
|
||||
|
@ -206,6 +207,7 @@ public:
|
|||
Core::System& m_system;
|
||||
PowerPC::PowerPCState& m_ppc_state;
|
||||
PowerPC::MMU& m_mmu;
|
||||
Core::BranchWatch& m_branch_watch;
|
||||
};
|
||||
|
||||
void JitTrampoline(JitBase& jit, u32 em_address);
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include "Common/CommonTypes.h"
|
||||
|
||||
#include "Core/CPUThreadConfigCallback.h"
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
#include "Core/Debugger/PPCDebugInterface.h"
|
||||
#include "Core/PowerPC/BreakPoints.h"
|
||||
#include "Core/PowerPC/ConditionRegister.h"
|
||||
|
@ -298,6 +299,8 @@ public:
|
|||
const MemChecks& GetMemChecks() const { return m_memchecks; }
|
||||
PPCDebugInterface& GetDebugInterface() { return m_debug_interface; }
|
||||
const PPCDebugInterface& GetDebugInterface() const { return m_debug_interface; }
|
||||
Core::BranchWatch& GetBranchWatch() { return m_branch_watch; }
|
||||
const Core::BranchWatch& GetBranchWatch() const { return m_branch_watch; }
|
||||
|
||||
private:
|
||||
void InitializeCPUCore(CPUCore cpu_core);
|
||||
|
@ -314,6 +317,7 @@ private:
|
|||
BreakPoints m_breakpoints;
|
||||
MemChecks m_memchecks;
|
||||
PPCDebugInterface m_debug_interface;
|
||||
Core::BranchWatch m_branch_watch;
|
||||
|
||||
CPUThreadConfigCallback::ConfigChangedCallbackID m_registered_config_callback_id;
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include "Common/IOFile.h"
|
||||
#include "Common/MsgHandler.h"
|
||||
#include "Common/Thread.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
#include "Common/Timer.h"
|
||||
#include "Common/Version.h"
|
||||
#include "Common/WorkQueueThread.h"
|
||||
|
@ -282,10 +283,12 @@ static std::string SystemTimeAsDoubleToString(double time)
|
|||
{
|
||||
// revert adjustments from GetSystemTimeAsDouble() to get a normal Unix timestamp again
|
||||
const time_t seconds = static_cast<time_t>(time) + DOUBLE_TIME_OFFSET;
|
||||
const tm local_time = fmt::localtime(seconds);
|
||||
const auto local_time = Common::Localtime(seconds);
|
||||
if (!local_time)
|
||||
return "";
|
||||
|
||||
// fmt is locale agnostic by default, so explicitly use current locale.
|
||||
return fmt::format(std::locale{""}, "{:%x %X}", local_time);
|
||||
return fmt::format(std::locale{""}, "{:%x %X}", *local_time);
|
||||
}
|
||||
|
||||
static std::string MakeStateFilename(int number);
|
||||
|
|
|
@ -52,8 +52,8 @@ struct System::Impl
|
|||
m_memory(system), m_pixel_engine{system}, m_power_pc(system),
|
||||
m_mmu(system, m_memory, m_power_pc), m_processor_interface(system),
|
||||
m_serial_interface(system), m_system_timers(system), m_video_interface(system),
|
||||
m_interpreter(system, m_power_pc.GetPPCState(), m_mmu), m_jit_interface(system),
|
||||
m_fifo_player(system), m_fifo_recorder(system), m_movie(system)
|
||||
m_interpreter(system, m_power_pc.GetPPCState(), m_mmu, m_power_pc.GetBranchWatch()),
|
||||
m_jit_interface(system), m_fifo_player(system), m_fifo_recorder(system), m_movie(system)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -141,9 +141,11 @@ public:
|
|||
bool IsPauseOnPanicMode() const { return m_pause_on_panic_enabled; }
|
||||
bool IsMIOS() const { return m_is_mios; }
|
||||
bool IsWii() const { return m_is_wii; }
|
||||
bool IsBranchWatchIgnoreApploader() { return m_branch_watch_ignore_apploader; }
|
||||
|
||||
void SetIsMIOS(bool is_mios) { m_is_mios = is_mios; }
|
||||
void SetIsWii(bool is_wii) { m_is_wii = is_wii; }
|
||||
void SetIsBranchWatchIgnoreApploader(bool enable) { m_branch_watch_ignore_apploader = enable; }
|
||||
|
||||
SoundStream* GetSoundStream() const;
|
||||
void SetSoundStream(std::unique_ptr<SoundStream> sound_stream);
|
||||
|
@ -202,5 +204,6 @@ private:
|
|||
bool m_pause_on_panic_enabled = false;
|
||||
bool m_is_mios = false;
|
||||
bool m_is_wii = false;
|
||||
bool m_branch_watch_ignore_apploader = false;
|
||||
};
|
||||
} // namespace Core
|
||||
|
|
|
@ -161,6 +161,7 @@
|
|||
<ClInclude Include="Common\SymbolDB.h" />
|
||||
<ClInclude Include="Common\Thread.h" />
|
||||
<ClInclude Include="Common\Timer.h" />
|
||||
<ClInclude Include="Common\TimeUtil.h" />
|
||||
<ClInclude Include="Common\TraversalClient.h" />
|
||||
<ClInclude Include="Common\TraversalProto.h" />
|
||||
<ClInclude Include="Common\TypeUtils.h" />
|
||||
|
@ -201,6 +202,7 @@
|
|||
<ClInclude Include="Core\Core.h" />
|
||||
<ClInclude Include="Core\CoreTiming.h" />
|
||||
<ClInclude Include="Core\CPUThreadConfigCallback.h" />
|
||||
<ClInclude Include="Core\Debugger\BranchWatch.h" />
|
||||
<ClInclude Include="Core\Debugger\CodeTrace.h" />
|
||||
<ClInclude Include="Core\Debugger\DebugInterface.h" />
|
||||
<ClInclude Include="Core\Debugger\Debugger_SymbolMap.h" />
|
||||
|
@ -832,6 +834,7 @@
|
|||
<ClCompile Include="Common\SymbolDB.cpp" />
|
||||
<ClCompile Include="Common\Thread.cpp" />
|
||||
<ClCompile Include="Common\Timer.cpp" />
|
||||
<ClCompile Include="Common\TimeUtil.cpp" />
|
||||
<ClCompile Include="Common\TraversalClient.cpp" />
|
||||
<ClCompile Include="Common\UPnP.cpp" />
|
||||
<ClCompile Include="Common\WindowsRegistry.cpp" />
|
||||
|
@ -866,6 +869,7 @@
|
|||
<ClCompile Include="Core\Core.cpp" />
|
||||
<ClCompile Include="Core\CoreTiming.cpp" />
|
||||
<ClCompile Include="Core\CPUThreadConfigCallback.cpp" />
|
||||
<ClCompile Include="Core\Debugger\BranchWatch.cpp" />
|
||||
<ClCompile Include="Core\Debugger\CodeTrace.cpp" />
|
||||
<ClCompile Include="Core\Debugger\Debugger_SymbolMap.cpp" />
|
||||
<ClCompile Include="Core\Debugger\Dump.cpp" />
|
||||
|
|
|
@ -150,10 +150,7 @@ void AchievementHeaderWidget::UpdateData()
|
|||
m_game_progress_hard->setVisible(false);
|
||||
m_game_progress_soft->setVisible(false);
|
||||
m_rich_presence->setVisible(false);
|
||||
if (instance.IsDisabled())
|
||||
{
|
||||
m_locked_warning->setVisible(true);
|
||||
}
|
||||
m_locked_warning->setVisible(instance.IsDisabled());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -206,12 +206,14 @@ add_executable(dolphin-emu
|
|||
Debugger/AssemblerWidget.h
|
||||
Debugger/AssemblyEditor.cpp
|
||||
Debugger/AssemblyEditor.h
|
||||
Debugger/BranchWatchDialog.cpp
|
||||
Debugger/BranchWatchDialog.h
|
||||
Debugger/BranchWatchTableModel.cpp
|
||||
Debugger/BranchWatchTableModel.h
|
||||
Debugger/BreakpointDialog.cpp
|
||||
Debugger/BreakpointDialog.h
|
||||
Debugger/BreakpointWidget.cpp
|
||||
Debugger/BreakpointWidget.h
|
||||
Debugger/CodeDiffDialog.cpp
|
||||
Debugger/CodeDiffDialog.h
|
||||
Debugger/CodeViewWidget.cpp
|
||||
Debugger/CodeViewWidget.h
|
||||
Debugger/CodeWidget.cpp
|
||||
|
|
|
@ -55,9 +55,9 @@ void GeneralWidget::CreateWidgets()
|
|||
m_video_layout = new QGridLayout();
|
||||
|
||||
m_backend_combo = new ToolTipComboBox();
|
||||
m_aspect_combo = new ConfigChoice(
|
||||
{tr("Auto"), tr("Force 16:9"), tr("Force 4:3"), tr("Stretch to Window"), tr("Custom")},
|
||||
Config::GFX_ASPECT_RATIO);
|
||||
m_aspect_combo = new ConfigChoice({tr("Auto"), tr("Force 16:9"), tr("Force 4:3"),
|
||||
tr("Stretch to Window"), tr("Custom"), tr("Custom (Stretch)")},
|
||||
Config::GFX_ASPECT_RATIO);
|
||||
m_custom_aspect_label = new QLabel(tr("Custom Aspect Ratio:"));
|
||||
m_custom_aspect_label->setHidden(true);
|
||||
constexpr int MAX_CUSTOM_ASPECT_RATIO_RESOLUTION = 10000;
|
||||
|
@ -155,7 +155,8 @@ void GeneralWidget::ConnectWidgets()
|
|||
emit BackendChanged(QString::fromStdString(Config::Get(Config::MAIN_GFX_BACKEND)));
|
||||
});
|
||||
connect(m_aspect_combo, qOverload<int>(&QComboBox::currentIndexChanged), this, [&](int index) {
|
||||
const bool is_custom_aspect_ratio = (index == static_cast<int>(AspectMode::Custom));
|
||||
const bool is_custom_aspect_ratio = (index == static_cast<int>(AspectMode::Custom)) ||
|
||||
(index == static_cast<int>(AspectMode::CustomStretch));
|
||||
m_custom_aspect_width->setEnabled(is_custom_aspect_ratio);
|
||||
m_custom_aspect_height->setEnabled(is_custom_aspect_ratio);
|
||||
m_custom_aspect_label->setHidden(!is_custom_aspect_ratio);
|
||||
|
@ -170,7 +171,9 @@ void GeneralWidget::LoadSettings()
|
|||
m_backend_combo->setCurrentIndex(m_backend_combo->findData(
|
||||
QVariant(QString::fromStdString(Config::Get(Config::MAIN_GFX_BACKEND)))));
|
||||
|
||||
const bool is_custom_aspect_ratio = (Config::Get(Config::GFX_ASPECT_RATIO) == AspectMode::Custom);
|
||||
const bool is_custom_aspect_ratio =
|
||||
(Config::Get(Config::GFX_ASPECT_RATIO) == AspectMode::Custom) ||
|
||||
(Config::Get(Config::GFX_ASPECT_RATIO) == AspectMode::CustomStretch);
|
||||
m_custom_aspect_width->setEnabled(is_custom_aspect_ratio);
|
||||
m_custom_aspect_height->setEnabled(is_custom_aspect_ratio);
|
||||
m_custom_aspect_label->setHidden(!is_custom_aspect_ratio);
|
||||
|
@ -247,15 +250,19 @@ void GeneralWidget::AddDescriptions()
|
|||
QT_TR_NOOP("Uses the main Dolphin window for rendering rather than "
|
||||
"a separate render window.<br><br><dolphin_emphasis>If unsure, leave "
|
||||
"this unchecked.</dolphin_emphasis>");
|
||||
static const char TR_ASPECT_RATIO_DESCRIPTION[] =
|
||||
QT_TR_NOOP("Selects which aspect ratio to use when rendering.<br>"
|
||||
"Each game can have a slightly different native aspect ratio."
|
||||
"<br><br>Auto: Uses the native aspect ratio"
|
||||
"<br>Force 16:9: Mimics an analog TV with a widescreen aspect ratio."
|
||||
"<br>Force 4:3: Mimics a standard 4:3 analog TV."
|
||||
"<br>Stretch to Window: Stretches the picture to the window size."
|
||||
"<br>Custom: For games running with specific custom aspect ratio cheats.<br><br>"
|
||||
"<dolphin_emphasis>If unsure, select Auto.</dolphin_emphasis>");
|
||||
static const char TR_ASPECT_RATIO_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Selects which aspect ratio to use when drawing on the render window.<br>"
|
||||
"Each game can have a slightly different native aspect ratio.<br>They can vary by "
|
||||
"scene and settings and rarely ever exactly match 4:3 or 16:9."
|
||||
"<br><br><b>Auto</b>: Uses the native aspect ratio"
|
||||
"<br><br><b>Force 16:9</b>: Mimics an analog TV with a widescreen aspect ratio."
|
||||
"<br><br><b>Force 4:3</b>: Mimics a standard 4:3 analog TV."
|
||||
"<br><br><b>Stretch to Window</b>: Stretches the picture to the window size."
|
||||
"<br><br><b>Custom</b>: Forces the specified aspect ratio."
|
||||
"<br>This is mostly intended to be used with aspect ratio cheats/mods."
|
||||
"<br><br><b>Custom (Stretch)</b>: Similar to `Custom` but not relative to the "
|
||||
"title's native aspect ratio.<br>This is not meant to be used under normal circumstances."
|
||||
"<br><br><dolphin_emphasis>If unsure, select Auto.</dolphin_emphasis>");
|
||||
static const char TR_VSYNC_DESCRIPTION[] = QT_TR_NOOP(
|
||||
"Waits for vertical blanks in order to prevent tearing.<br><br>Decreases performance "
|
||||
"if emulation speed is below 100%.<br><br><dolphin_emphasis>If unsure, leave "
|
||||
|
|
|
@ -6,24 +6,25 @@
|
|||
#include <memory>
|
||||
|
||||
#include <QBitmap>
|
||||
#include <QGraphicsEffect>
|
||||
#include <QGraphicsView>
|
||||
#include <QGridLayout>
|
||||
#include <QBrush>
|
||||
#include <QCursor>
|
||||
#include <QFont>
|
||||
#include <QGuiApplication>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QStyle>
|
||||
|
||||
#include <QPen>
|
||||
#include <QPoint>
|
||||
#include <QRect>
|
||||
#include <QScreen>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <QToolTip>
|
||||
#endif
|
||||
|
||||
#include "Core/Config/MainSettings.h"
|
||||
|
||||
#include "DolphinQt/Settings.h"
|
||||
|
||||
namespace
|
||||
|
@ -31,8 +32,9 @@ namespace
|
|||
std::unique_ptr<BalloonTip> s_the_balloon_tip = nullptr;
|
||||
} // namespace
|
||||
|
||||
void BalloonTip::ShowBalloon(const QIcon& icon, const QString& title, const QString& message,
|
||||
const QPoint& pos, QWidget* parent, ShowArrow show_arrow)
|
||||
void BalloonTip::ShowBalloon(const QString& title, const QString& message,
|
||||
const QPoint& target_arrow_tip_position, QWidget* const parent,
|
||||
const ShowArrow show_arrow, const int border_width)
|
||||
{
|
||||
HideBalloon();
|
||||
if (message.isEmpty() && title.isEmpty())
|
||||
|
@ -42,10 +44,10 @@ void BalloonTip::ShowBalloon(const QIcon& icon, const QString& title, const QStr
|
|||
QString the_message = message;
|
||||
the_message.replace(QStringLiteral("<dolphin_emphasis>"), QStringLiteral("<b>"));
|
||||
the_message.replace(QStringLiteral("</dolphin_emphasis>"), QStringLiteral("</b>"));
|
||||
QToolTip::showText(pos, the_message, parent);
|
||||
QToolTip::showText(target_arrow_tip_position, the_message, parent);
|
||||
#else
|
||||
s_the_balloon_tip = std::make_unique<BalloonTip>(PrivateTag{}, icon, title, message, parent);
|
||||
s_the_balloon_tip->UpdateBoundsAndRedraw(pos, show_arrow);
|
||||
s_the_balloon_tip = std::make_unique<BalloonTip>(PrivateTag{}, title, message, parent);
|
||||
s_the_balloon_tip->UpdateBoundsAndRedraw(target_arrow_tip_position, show_arrow, border_width);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -54,20 +56,16 @@ void BalloonTip::HideBalloon()
|
|||
#if defined(__APPLE__)
|
||||
QToolTip::hideText();
|
||||
#else
|
||||
if (!s_the_balloon_tip)
|
||||
if (s_the_balloon_tip == nullptr)
|
||||
return;
|
||||
s_the_balloon_tip->hide();
|
||||
s_the_balloon_tip.reset();
|
||||
#endif
|
||||
}
|
||||
|
||||
BalloonTip::BalloonTip(PrivateTag, const QIcon& icon, QString title, QString message,
|
||||
QWidget* parent)
|
||||
BalloonTip::BalloonTip(PrivateTag, const QString& title, QString message, QWidget* const parent)
|
||||
: QWidget(nullptr, Qt::ToolTip)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose);
|
||||
setAutoFillBackground(true);
|
||||
|
||||
QColor window_color;
|
||||
QColor text_color;
|
||||
QColor dolphin_emphasis;
|
||||
|
@ -78,43 +76,41 @@ BalloonTip::BalloonTip(PrivateTag, const QIcon& icon, QString title, QString mes
|
|||
.arg(text_color.rgba(), 0, 16);
|
||||
setStyleSheet(style_sheet);
|
||||
|
||||
// Replace text in our our message
|
||||
// if specific "tags" are used
|
||||
// Replace text in our our message if specific "tags" are used
|
||||
message.replace(QStringLiteral("<dolphin_emphasis>"),
|
||||
QStringLiteral("<font color=\"#%1\"><b>").arg(dolphin_emphasis.rgba(), 0, 16));
|
||||
message.replace(QStringLiteral("</dolphin_emphasis>"), QStringLiteral("</b></font>"));
|
||||
|
||||
auto* title_label = new QLabel;
|
||||
title_label->installEventFilter(this);
|
||||
title_label->setText(title);
|
||||
QFont f = title_label->font();
|
||||
f.setBold(true);
|
||||
title_label->setFont(f);
|
||||
title_label->setTextFormat(Qt::RichText);
|
||||
title_label->setSizePolicy(QSizePolicy::Policy::MinimumExpanding,
|
||||
QSizePolicy::Policy::MinimumExpanding);
|
||||
auto* const balloontip_layout = new QVBoxLayout;
|
||||
balloontip_layout->setSizeConstraint(QLayout::SetFixedSize);
|
||||
setLayout(balloontip_layout);
|
||||
|
||||
auto* message_label = new QLabel;
|
||||
message_label->installEventFilter(this);
|
||||
message_label->setText(message);
|
||||
message_label->setTextFormat(Qt::RichText);
|
||||
message_label->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||
const auto create_label = [=](const QString& text) {
|
||||
QLabel* const label = new QLabel;
|
||||
balloontip_layout->addWidget(label);
|
||||
|
||||
const int limit = message_label->screen()->availableGeometry().width() / 3;
|
||||
message_label->setMaximumWidth(limit);
|
||||
message_label->setSizePolicy(QSizePolicy::Policy::MinimumExpanding,
|
||||
QSizePolicy::Policy::MinimumExpanding);
|
||||
if (message_label->sizeHint().width() > limit)
|
||||
label->setText(text);
|
||||
label->setTextFormat(Qt::RichText);
|
||||
|
||||
const int max_width = label->screen()->availableGeometry().width() / 3;
|
||||
label->setMaximumWidth(max_width);
|
||||
if (label->sizeHint().width() > max_width)
|
||||
label->setWordWrap(true);
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
if (!title.isEmpty())
|
||||
{
|
||||
message_label->setWordWrap(true);
|
||||
QLabel* const title_label = create_label(title);
|
||||
|
||||
QFont title_font = title_label->font();
|
||||
title_font.setBold(true);
|
||||
title_label->setFont(title_font);
|
||||
}
|
||||
|
||||
auto* layout = new QGridLayout;
|
||||
layout->addWidget(title_label, 0, 0, 1, 2);
|
||||
|
||||
layout->addWidget(message_label, 1, 0, 1, 3);
|
||||
layout->setSizeConstraint(QLayout::SetMinimumSize);
|
||||
setLayout(layout);
|
||||
if (!message.isEmpty())
|
||||
create_label(message);
|
||||
}
|
||||
|
||||
void BalloonTip::paintEvent(QPaintEvent*)
|
||||
|
@ -123,116 +119,215 @@ void BalloonTip::paintEvent(QPaintEvent*)
|
|||
painter.drawPixmap(rect(), m_pixmap);
|
||||
}
|
||||
|
||||
void BalloonTip::UpdateBoundsAndRedraw(const QPoint& pos, ShowArrow show_arrow)
|
||||
void BalloonTip::UpdateBoundsAndRedraw(const QPoint& target_arrow_tip_position,
|
||||
const ShowArrow show_arrow, const int border_full_width)
|
||||
{
|
||||
m_show_arrow = show_arrow == ShowArrow::Yes;
|
||||
const float border_half_width = border_full_width / 2.0;
|
||||
|
||||
QScreen* screen = QGuiApplication::screenAt(pos);
|
||||
if (!screen)
|
||||
screen = QGuiApplication::primaryScreen();
|
||||
// This should be odd so that the arrow tip is a single pixel wide.
|
||||
const int arrow_full_width = 35;
|
||||
const float arrow_half_width = arrow_full_width / 2.0;
|
||||
// The y distance between the inner edge of the rectangle border and the inner tip of the arrow
|
||||
// border, and also the distance between the outer edge of the rectangle border and the outer tip
|
||||
// of the arrow border
|
||||
const int arrow_height = (1 + arrow_full_width) / 2;
|
||||
|
||||
// Distance between the label layout and the inner rectangle border edge
|
||||
const int balloon_interior_padding = 12;
|
||||
// Prevent the corners of the label layout from portruding into the rounded rectangle corners at
|
||||
// larger border sizes.
|
||||
const int rounded_corner_margin = border_half_width / 4;
|
||||
const int horizontal_margin =
|
||||
border_full_width + rounded_corner_margin + balloon_interior_padding;
|
||||
const int vertical_margin = horizontal_margin + arrow_height;
|
||||
|
||||
// Create enough space around the layout containing the title and message to draw the balloon and
|
||||
// both arrow tips (at most one of which will be visible)
|
||||
layout()->setContentsMargins(horizontal_margin, vertical_margin, horizontal_margin,
|
||||
vertical_margin);
|
||||
|
||||
QSize size_hint = sizeHint();
|
||||
|
||||
// These positions represent the middle of each edge of the BalloonTip's rounded rectangle
|
||||
const float rect_width = size_hint.width() - border_full_width;
|
||||
const float rect_height = size_hint.height() - border_full_width - 2 * arrow_height;
|
||||
const float rect_top = border_half_width + arrow_height;
|
||||
const float rect_bottom = rect_top + rect_height;
|
||||
const float rect_left = border_half_width;
|
||||
// rect_right isn't used for anything
|
||||
|
||||
// Qt defines the radius of a rounded rectangle as "the radius of the ellipses defining the
|
||||
// corner". Unlike the rectangle's edges this corresponds to the outside of the rounded curve
|
||||
// instead of its middle, so we add the full width to the inner radius instead of the half width
|
||||
const float corner_base_inner_radius = 7.0;
|
||||
const float corner_outer_radius = corner_base_inner_radius + border_full_width;
|
||||
|
||||
// This value is arbitrary but works well.
|
||||
const int base_arrow_x_offset = 34;
|
||||
// Adjust the offset inward to compensate for the border and rounded corner widths. This ensures
|
||||
// the arrow is on the flat part of the top/bottom border.
|
||||
const int adjusted_arrow_x_offset =
|
||||
base_arrow_x_offset + border_full_width + static_cast<int>(border_half_width);
|
||||
// If the border is wide enough (or the BalloonTip small enough) the offset might end up past the
|
||||
// midpoint; if that happens just use the midpoint instead
|
||||
const int centered_arrow_x_offset = (size_hint.width() - arrow_full_width) / 2;
|
||||
// If the arrow is on the left this is the distance between the left edge of the BalloonTip and
|
||||
// the left edge of the arrow interior; otherwise the distance between the right edges.
|
||||
const int arrow_nearest_edge_x_offset =
|
||||
std::min(adjusted_arrow_x_offset, centered_arrow_x_offset);
|
||||
const int arrow_tip_x_offset = arrow_nearest_edge_x_offset + arrow_half_width;
|
||||
|
||||
// The BalloonTip should be contained entirely within the screen that contains the target
|
||||
// position.
|
||||
QScreen* screen = QGuiApplication::screenAt(target_arrow_tip_position);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
// If the target position isn't on any screen (which can happen if the window is partly off the
|
||||
// screen and the user hovers over the label) then use the screen containing the cursor instead.
|
||||
screen = QGuiApplication::screenAt(QCursor::pos());
|
||||
}
|
||||
const QRect screen_rect = screen->geometry();
|
||||
|
||||
QSize sh = sizeHint();
|
||||
// The look should resemble the default tooltip style set in Settings::ApplyStyle()
|
||||
const int border = 1;
|
||||
const int arrow_height = 18;
|
||||
const int arrow_width = 18;
|
||||
const int arrow_offset = 52;
|
||||
const int rect_center = 7;
|
||||
const bool arrow_at_bottom = (pos.y() - sh.height() - arrow_height > 0);
|
||||
const bool arrow_at_left = (pos.x() + sh.width() - arrow_width < screen_rect.width());
|
||||
const int default_padding = 10;
|
||||
layout()->setContentsMargins(border + 3 + default_padding,
|
||||
border + (arrow_at_bottom ? 0 : arrow_height) + 2 + default_padding,
|
||||
border + 3 + default_padding,
|
||||
border + (arrow_at_bottom ? arrow_height : 0) + 2 + default_padding);
|
||||
updateGeometry();
|
||||
sh = sizeHint();
|
||||
QPainterPath rect_path;
|
||||
rect_path.addRoundedRect(rect_left, rect_top, rect_width, rect_height, corner_outer_radius,
|
||||
corner_outer_radius);
|
||||
|
||||
int ml, mr, mt, mb;
|
||||
QSize sz = sizeHint();
|
||||
if (arrow_at_bottom)
|
||||
// The default pen cap style Qt::SquareCap extends drawn lines one half width before the starting
|
||||
// point and one half width after the ending point such that the starting and ending points are
|
||||
// surrounded by drawn pixels in both dimensions instead of just for the width. This is a
|
||||
// reasonable default but we need to draw lines precisely, and this behavior causes problems when
|
||||
// drawing lines of length and width 1 (which we do for the arrow interior, and also the arrow
|
||||
// border when border_full_width is 1). For those lines to correctly end up as a pixel we would
|
||||
// need to offset the start and end points by 0.5 inward. However, doing that would lead them to
|
||||
// be at the same point, and if the endpoints of a line are the same Qt simply doesn't draw it
|
||||
// regardless of the cap style.
|
||||
//
|
||||
// Using Qt::FlatCap instead fixes the issue.
|
||||
|
||||
m_pixmap = QPixmap(size_hint);
|
||||
|
||||
QPen border_pen(m_border_color, border_full_width);
|
||||
border_pen.setCapStyle(Qt::FlatCap);
|
||||
|
||||
QPainter balloon_painter(&m_pixmap);
|
||||
balloon_painter.setPen(border_pen);
|
||||
balloon_painter.setBrush(palette().color(QPalette::Window));
|
||||
balloon_painter.drawPath(rect_path);
|
||||
|
||||
QBitmap mask_bitmap(size_hint);
|
||||
mask_bitmap.fill(Qt::color0);
|
||||
|
||||
QPen mask_pen(Qt::color1, border_full_width);
|
||||
mask_pen.setCapStyle(Qt::FlatCap);
|
||||
|
||||
QPainter mask_painter(&mask_bitmap);
|
||||
mask_painter.setPen(mask_pen);
|
||||
mask_painter.setBrush(QBrush(Qt::color1));
|
||||
mask_painter.drawPath(rect_path);
|
||||
|
||||
const bool arrow_at_bottom =
|
||||
target_arrow_tip_position.y() - size_hint.height() + arrow_height >= 0;
|
||||
const bool arrow_at_left =
|
||||
target_arrow_tip_position.x() + size_hint.width() - arrow_tip_x_offset < screen_rect.width();
|
||||
|
||||
const float arrow_base_y =
|
||||
arrow_at_bottom ? rect_bottom - border_half_width : rect_top + border_half_width;
|
||||
|
||||
const float arrow_tip_vertical_offset = arrow_at_bottom ? arrow_height : -arrow_height;
|
||||
const float arrow_tip_interior_y = arrow_base_y + arrow_tip_vertical_offset;
|
||||
const float arrow_tip_exterior_y =
|
||||
arrow_tip_interior_y + (arrow_at_bottom ? border_full_width : -border_full_width);
|
||||
const float arrow_base_left_edge_x =
|
||||
arrow_at_left ? arrow_nearest_edge_x_offset :
|
||||
size_hint.width() - arrow_nearest_edge_x_offset - arrow_full_width;
|
||||
const float arrow_base_right_edge_x = arrow_base_left_edge_x + arrow_full_width;
|
||||
const float arrow_tip_x = arrow_base_left_edge_x + arrow_half_width;
|
||||
|
||||
if (show_arrow == ShowArrow::Yes)
|
||||
{
|
||||
ml = mt = 0;
|
||||
mr = sz.width() - 1;
|
||||
mb = sz.height() - arrow_height - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
ml = 0;
|
||||
mt = arrow_height;
|
||||
mr = sz.width() - 1;
|
||||
mb = sz.height() - 1;
|
||||
// Drawing diagonal lines in Qt is filled with edge cases and inexplicable behavior. Getting it
|
||||
// to do what you want at one border size is simple enough, but doing so flexibly is an exercise
|
||||
// in futility. Some examples:
|
||||
// * For some values of x, diagonal lines of width x and x+1 are drawn exactly the same.
|
||||
// * When drawing a triangle where p1 and p3 have exactly the same y value, they can be drawn at
|
||||
// different heights.
|
||||
// * Lines of width 1 sometimes get drawn one pixel past where they should even with FlatCap,
|
||||
// but only on the left side (regardless of which direction the stroke was).
|
||||
//
|
||||
// Instead of dealing with all that, fake it with vertical lines which are much better behaved.
|
||||
// Draw a bunch of vertical lines with width 1 to form the arrow border and interior.
|
||||
|
||||
QPainterPath arrow_border_path;
|
||||
QPainterPath arrow_interior_fill_path;
|
||||
const float y_end_offset = arrow_at_bottom ? border_full_width : -border_full_width;
|
||||
|
||||
// Draw the arrow border and interior lines from the outside inward. Each loop iteration draws
|
||||
// one pair of border lines and one pair of interior lines.
|
||||
for (int i = 1; i <= arrow_half_width; i++)
|
||||
{
|
||||
const float x_offset_from_arrow_base_edge = i - 0.5;
|
||||
const float border_y_start = arrow_base_y + (arrow_at_bottom ? i : -i);
|
||||
const float border_y_end = border_y_start + y_end_offset;
|
||||
const float interior_y_start = arrow_base_y;
|
||||
const float interior_y_end = border_y_start;
|
||||
const float left_line_x = arrow_base_left_edge_x + x_offset_from_arrow_base_edge;
|
||||
const float right_line_x = arrow_base_right_edge_x - x_offset_from_arrow_base_edge;
|
||||
|
||||
arrow_border_path.moveTo(left_line_x, border_y_start);
|
||||
arrow_border_path.lineTo(left_line_x, border_y_end);
|
||||
|
||||
arrow_border_path.moveTo(right_line_x, border_y_start);
|
||||
arrow_border_path.lineTo(right_line_x, border_y_end);
|
||||
|
||||
arrow_interior_fill_path.moveTo(left_line_x, interior_y_start);
|
||||
arrow_interior_fill_path.lineTo(left_line_x, interior_y_end);
|
||||
|
||||
arrow_interior_fill_path.moveTo(right_line_x, interior_y_start);
|
||||
arrow_interior_fill_path.lineTo(right_line_x, interior_y_end);
|
||||
}
|
||||
// The middle border line
|
||||
arrow_border_path.moveTo(arrow_tip_x, arrow_tip_interior_y);
|
||||
arrow_border_path.lineTo(arrow_tip_x, arrow_tip_interior_y + y_end_offset);
|
||||
|
||||
// The middle interior line
|
||||
arrow_interior_fill_path.moveTo(arrow_tip_x, arrow_base_y);
|
||||
arrow_interior_fill_path.lineTo(arrow_tip_x, arrow_tip_interior_y);
|
||||
|
||||
border_pen.setWidth(1);
|
||||
|
||||
balloon_painter.setPen(border_pen);
|
||||
balloon_painter.drawPath(arrow_border_path);
|
||||
|
||||
QPen arrow_interior_fill_pen(palette().color(QPalette::Window), 1);
|
||||
arrow_interior_fill_pen.setCapStyle(Qt::FlatCap);
|
||||
balloon_painter.setPen(arrow_interior_fill_pen);
|
||||
balloon_painter.drawPath(arrow_interior_fill_path);
|
||||
|
||||
mask_pen.setWidth(1);
|
||||
mask_painter.setPen(mask_pen);
|
||||
|
||||
mask_painter.drawPath(arrow_border_path);
|
||||
mask_painter.drawPath(arrow_interior_fill_path);
|
||||
}
|
||||
|
||||
QPainterPath path;
|
||||
path.moveTo(ml + rect_center, mt);
|
||||
if (!arrow_at_bottom && arrow_at_left)
|
||||
{
|
||||
if (m_show_arrow)
|
||||
{
|
||||
path.lineTo(ml + arrow_offset - arrow_width, mt);
|
||||
path.lineTo(ml + arrow_offset, mt - arrow_height);
|
||||
path.lineTo(ml + arrow_offset + arrow_width, mt);
|
||||
}
|
||||
move(qMax(pos.x() - arrow_offset, screen_rect.left() + 2), pos.y());
|
||||
}
|
||||
else if (!arrow_at_bottom && !arrow_at_left)
|
||||
{
|
||||
if (m_show_arrow)
|
||||
{
|
||||
path.lineTo(mr - arrow_offset - arrow_width, mt);
|
||||
path.lineTo(mr - arrow_offset, mt - arrow_height);
|
||||
path.lineTo(mr - arrow_offset + arrow_width, mt);
|
||||
}
|
||||
move(qMin(pos.x() - sh.width() + arrow_offset, screen_rect.right() - sh.width() - 2), pos.y());
|
||||
}
|
||||
path.lineTo(mr - rect_center, mt);
|
||||
path.arcTo(QRect(mr - rect_center * 2, mt, rect_center * 2, rect_center * 2), 90, -90);
|
||||
path.lineTo(mr, mb - rect_center);
|
||||
path.arcTo(QRect(mr - rect_center * 2, mb - rect_center * 2, rect_center * 2, rect_center * 2), 0,
|
||||
-90);
|
||||
if (arrow_at_bottom && !arrow_at_left)
|
||||
{
|
||||
if (m_show_arrow)
|
||||
{
|
||||
path.lineTo(mr - arrow_offset + arrow_width, mb);
|
||||
path.lineTo(mr - arrow_offset, mb + arrow_height);
|
||||
path.lineTo(mr - arrow_offset - arrow_width, mb);
|
||||
}
|
||||
move(qMin(pos.x() - sh.width() + arrow_offset, screen_rect.right() - sh.width() - 2),
|
||||
pos.y() - sh.height());
|
||||
}
|
||||
else if (arrow_at_bottom && arrow_at_left)
|
||||
{
|
||||
if (m_show_arrow)
|
||||
{
|
||||
path.lineTo(arrow_offset + arrow_width, mb);
|
||||
path.lineTo(arrow_offset, mb + arrow_height);
|
||||
path.lineTo(arrow_offset - arrow_width, mb);
|
||||
}
|
||||
move(qMax(pos.x() - arrow_offset, screen_rect.x() + 2), pos.y() - sh.height());
|
||||
}
|
||||
path.lineTo(ml + rect_center, mb);
|
||||
path.arcTo(QRect(ml, mb - rect_center * 2, rect_center * 2, rect_center * 2), -90, -90);
|
||||
path.lineTo(ml, mt + rect_center);
|
||||
path.arcTo(QRect(ml, mt, rect_center * 2, rect_center * 2), 180, -90);
|
||||
setMask(mask_bitmap);
|
||||
|
||||
// Set the mask
|
||||
QBitmap bitmap(sizeHint());
|
||||
bitmap.fill(Qt::color0);
|
||||
QPainter painter1(&bitmap);
|
||||
painter1.setPen(QPen(Qt::color1, border));
|
||||
painter1.setBrush(QBrush(Qt::color1));
|
||||
painter1.drawPath(path);
|
||||
setMask(bitmap);
|
||||
// Place the arrow tip at the target position whether the arrow tip is drawn or not
|
||||
const int target_balloontip_global_x =
|
||||
target_arrow_tip_position.x() - static_cast<int>(arrow_tip_x);
|
||||
const int rightmost_valid_balloontip_global_x = screen_rect.width() - size_hint.width();
|
||||
// If the balloon would extend off the screen, push it left or right until it's not
|
||||
const int actual_balloontip_global_x =
|
||||
std::max(0, std::min(rightmost_valid_balloontip_global_x, target_balloontip_global_x));
|
||||
// The tip pixel should be in the middle of the control, and arrow_tip_exterior_y is at the bottom
|
||||
// of that pixel. When arrow_at_bottom is true the arrow is above arrow_tip_exterior_y and so the
|
||||
// tip pixel is in the right place, but when it's false the arrow is below arrow_tip_exterior_y
|
||||
// so the tip pixel would be the one below that. Make this adjustment to fix that.
|
||||
const int tip_pixel_adjustment = arrow_at_bottom ? 0 : 1;
|
||||
const int actual_balloontip_global_y =
|
||||
target_arrow_tip_position.y() - arrow_tip_exterior_y - tip_pixel_adjustment;
|
||||
|
||||
// Draw the border
|
||||
m_pixmap = QPixmap(sz);
|
||||
QPainter painter2(&m_pixmap);
|
||||
painter2.setPen(QPen(m_border_color));
|
||||
painter2.setBrush(palette().color(QPalette::Window));
|
||||
painter2.drawPath(path);
|
||||
move(actual_balloontip_global_x, actual_balloontip_global_y);
|
||||
|
||||
show();
|
||||
}
|
||||
|
|
|
@ -3,10 +3,14 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QIcon>
|
||||
#include <QColor>
|
||||
#include <QPixmap>
|
||||
#include <QWidget>
|
||||
|
||||
class QPaintEvent;
|
||||
class QPoint;
|
||||
class QString;
|
||||
|
||||
class BalloonTip : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -21,15 +25,16 @@ public:
|
|||
Yes,
|
||||
No
|
||||
};
|
||||
static void ShowBalloon(const QIcon& icon, const QString& title, const QString& msg,
|
||||
const QPoint& pos, QWidget* parent,
|
||||
ShowArrow show_arrow = ShowArrow::Yes);
|
||||
static void ShowBalloon(const QString& title, const QString& message,
|
||||
const QPoint& target_arrow_tip_position, QWidget* parent,
|
||||
ShowArrow show_arrow = ShowArrow::Yes, int border_width = 1);
|
||||
static void HideBalloon();
|
||||
|
||||
BalloonTip(PrivateTag, const QIcon& icon, QString title, QString msg, QWidget* parent);
|
||||
BalloonTip(PrivateTag, const QString& title, QString message, QWidget* parent);
|
||||
|
||||
private:
|
||||
void UpdateBoundsAndRedraw(const QPoint&, ShowArrow);
|
||||
void UpdateBoundsAndRedraw(const QPoint& target_arrow_tip_position, ShowArrow show_arrow,
|
||||
int border_width);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
@ -37,5 +42,4 @@ protected:
|
|||
private:
|
||||
QColor m_border_color;
|
||||
QPixmap m_pixmap;
|
||||
bool m_show_arrow = true;
|
||||
};
|
||||
|
|
|
@ -37,7 +37,7 @@ private:
|
|||
this->killTimer(*m_timer_id);
|
||||
m_timer_id.reset();
|
||||
|
||||
BalloonTip::ShowBalloon(QIcon(), m_title, m_description,
|
||||
BalloonTip::ShowBalloon(m_title, m_description,
|
||||
this->parentWidget()->mapToGlobal(GetToolTipPosition()), this);
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,114 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QModelIndexList>
|
||||
|
||||
#include "Core/Core.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class BranchWatch;
|
||||
class CPUThreadGuard;
|
||||
class System;
|
||||
} // namespace Core
|
||||
class BranchWatchProxyModel;
|
||||
class BranchWatchTableModel;
|
||||
class CodeWidget;
|
||||
class QAction;
|
||||
class QMenu;
|
||||
class QPoint;
|
||||
class QPushButton;
|
||||
class QStatusBar;
|
||||
class QTableView;
|
||||
class QTimer;
|
||||
class QToolBar;
|
||||
class QWidget;
|
||||
|
||||
namespace BranchWatchTableModelColumn
|
||||
{
|
||||
enum EnumType : int;
|
||||
}
|
||||
namespace BranchWatchTableModelUserRole
|
||||
{
|
||||
enum EnumType : int;
|
||||
}
|
||||
|
||||
class BranchWatchDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using Column = BranchWatchTableModelColumn::EnumType;
|
||||
using UserRole = BranchWatchTableModelUserRole::EnumType;
|
||||
|
||||
public:
|
||||
explicit BranchWatchDialog(Core::System& system, Core::BranchWatch& branch_watch,
|
||||
CodeWidget* code_widget, QWidget* parent = nullptr);
|
||||
void done(int r) override;
|
||||
int exec() override;
|
||||
void open() override;
|
||||
|
||||
private:
|
||||
void OnStartPause(bool checked);
|
||||
void OnClearBranchWatch();
|
||||
void OnSave();
|
||||
void OnSaveAs();
|
||||
void OnLoad();
|
||||
void OnLoadFrom();
|
||||
void OnCodePathWasTaken();
|
||||
void OnCodePathNotTaken();
|
||||
void OnBranchWasOverwritten();
|
||||
void OnBranchNotOverwritten();
|
||||
void OnWipeRecentHits();
|
||||
void OnWipeInspection();
|
||||
void OnTimeout();
|
||||
void OnEmulationStateChanged(Core::State new_state);
|
||||
void OnHelp();
|
||||
void OnToggleAutoSave(bool checked);
|
||||
void OnHideShowControls(bool checked);
|
||||
void OnToggleIgnoreApploader(bool checked);
|
||||
|
||||
void OnTableClicked(const QModelIndex& index);
|
||||
void OnTableContextMenu(const QPoint& pos);
|
||||
void OnTableHeaderContextMenu(const QPoint& pos);
|
||||
void OnTableDelete(QModelIndexList index_list);
|
||||
void OnTableDeleteKeypress();
|
||||
void OnTableSetBLR(QModelIndexList index_list);
|
||||
void OnTableSetNOP(QModelIndexList index_list);
|
||||
void OnTableCopyAddress(QModelIndexList index_list);
|
||||
|
||||
public:
|
||||
// TODO: Step doesn't cause EmulationStateChanged to be emitted, so it has to call this manually.
|
||||
void Update();
|
||||
// TODO: There seems to be a lack of a ubiquitous signal for when symbols change.
|
||||
void UpdateSymbols();
|
||||
|
||||
private:
|
||||
void UpdateStatus();
|
||||
void Save(const Core::CPUThreadGuard& guard, const std::string& filepath);
|
||||
void Load(const Core::CPUThreadGuard& guard, const std::string& filepath);
|
||||
void AutoSave(const Core::CPUThreadGuard& guard);
|
||||
|
||||
Core::System& m_system;
|
||||
Core::BranchWatch& m_branch_watch;
|
||||
CodeWidget* m_code_widget;
|
||||
|
||||
QPushButton *m_btn_start_pause, *m_btn_clear_watch, *m_btn_path_was_taken, *m_btn_path_not_taken,
|
||||
*m_btn_was_overwritten, *m_btn_not_overwritten, *m_btn_wipe_recent_hits;
|
||||
QAction* m_act_autosave;
|
||||
QMenu* m_mnu_column_visibility;
|
||||
|
||||
QToolBar* m_control_toolbar;
|
||||
QTableView* m_table_view;
|
||||
BranchWatchProxyModel* m_table_proxy;
|
||||
BranchWatchTableModel* m_table_model;
|
||||
QStatusBar* m_status_bar;
|
||||
QTimer* m_timer;
|
||||
|
||||
std::optional<std::string> m_autosave_filepath;
|
||||
};
|
|
@ -0,0 +1,502 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Debugger/BranchWatchTableModel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
|
||||
#include <QBrush>
|
||||
|
||||
#include "Common/Assert.h"
|
||||
#include "Common/GekkoDisassembler.h"
|
||||
#include "Core/Debugger/BranchWatch.h"
|
||||
#include "Core/PowerPC/PPCSymbolDB.h"
|
||||
|
||||
QVariant BranchWatchTableModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case Qt::DisplayRole:
|
||||
return DisplayRoleData(index);
|
||||
case Qt::FontRole:
|
||||
return FontRoleData(index);
|
||||
case Qt::TextAlignmentRole:
|
||||
return TextAlignmentRoleData(index);
|
||||
case Qt::ForegroundRole:
|
||||
return ForegroundRoleData(index);
|
||||
case UserRole::ClickRole:
|
||||
return ClickRoleData(index);
|
||||
case UserRole::SortRole:
|
||||
return SortRoleData(index);
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant BranchWatchTableModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (orientation == Qt::Vertical || role != Qt::DisplayRole)
|
||||
return QVariant();
|
||||
|
||||
static constexpr std::array<const char*, Column::NumberOfColumns> headers = {
|
||||
QT_TR_NOOP("Instr."), QT_TR_NOOP("Cond."),
|
||||
QT_TR_NOOP("Origin"), QT_TR_NOOP("Destination"),
|
||||
QT_TR_NOOP("Recent Hits"), QT_TR_NOOP("Total Hits"),
|
||||
QT_TR_NOOP("Origin Symbol"), QT_TR_NOOP("Destination Symbol")};
|
||||
return tr(headers[section]);
|
||||
}
|
||||
|
||||
int BranchWatchTableModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return static_cast<int>(m_branch_watch.GetSelection().size());
|
||||
}
|
||||
|
||||
int BranchWatchTableModel::columnCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return Column::NumberOfColumns;
|
||||
}
|
||||
|
||||
bool BranchWatchTableModel::removeRows(int row, int count, const QModelIndex& parent)
|
||||
{
|
||||
if (parent.isValid() || row < 0)
|
||||
return false;
|
||||
if (count <= 0)
|
||||
return true;
|
||||
|
||||
auto& selection = m_branch_watch.GetSelection();
|
||||
beginRemoveRows(parent, row, row + count - 1); // Last is inclusive in Qt!
|
||||
selection.erase(selection.begin() + row, selection.begin() + row + count);
|
||||
m_symbol_list.remove(row, count);
|
||||
endRemoveRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::OnClearBranchWatch(const Core::CPUThreadGuard& guard)
|
||||
{
|
||||
emit layoutAboutToBeChanged();
|
||||
m_branch_watch.Clear(guard);
|
||||
m_symbol_list.clear();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::OnCodePathWasTaken(const Core::CPUThreadGuard& guard)
|
||||
{
|
||||
emit layoutAboutToBeChanged();
|
||||
m_branch_watch.IsolateHasExecuted(guard);
|
||||
PrefetchSymbols();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::OnCodePathNotTaken(const Core::CPUThreadGuard& guard)
|
||||
{
|
||||
emit layoutAboutToBeChanged();
|
||||
m_branch_watch.IsolateNotExecuted(guard);
|
||||
PrefetchSymbols();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::OnBranchWasOverwritten(const Core::CPUThreadGuard& guard)
|
||||
{
|
||||
emit layoutAboutToBeChanged();
|
||||
m_branch_watch.IsolateWasOverwritten(guard);
|
||||
PrefetchSymbols();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::OnBranchNotOverwritten(const Core::CPUThreadGuard& guard)
|
||||
{
|
||||
emit layoutAboutToBeChanged();
|
||||
m_branch_watch.IsolateNotOverwritten(guard);
|
||||
PrefetchSymbols();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::OnWipeRecentHits()
|
||||
{
|
||||
const int row_count = rowCount();
|
||||
if (row_count <= 0)
|
||||
return;
|
||||
static const QList<int> roles = {Qt::DisplayRole};
|
||||
m_branch_watch.UpdateHitsSnapshot();
|
||||
const int last = row_count - 1;
|
||||
emit dataChanged(createIndex(0, Column::RecentHits), createIndex(last, Column::RecentHits),
|
||||
roles);
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::OnWipeInspection()
|
||||
{
|
||||
const int row_count = rowCount();
|
||||
if (row_count <= 0)
|
||||
return;
|
||||
static const QList<int> roles = {Qt::FontRole, Qt::ForegroundRole};
|
||||
m_branch_watch.ClearSelectionInspection();
|
||||
const int last = row_count - 1;
|
||||
emit dataChanged(createIndex(0, Column::Origin), createIndex(last, Column::Destination), roles);
|
||||
emit dataChanged(createIndex(0, Column::OriginSymbol), createIndex(last, Column::DestinSymbol),
|
||||
roles);
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::OnDelete(QModelIndexList index_list)
|
||||
{
|
||||
std::sort(index_list.begin(), index_list.end());
|
||||
// TODO C++20: std::ranges::reverse_view
|
||||
for (auto iter = index_list.rbegin(); iter != index_list.rend(); ++iter)
|
||||
{
|
||||
if (!iter->isValid())
|
||||
continue;
|
||||
removeRow(iter->row());
|
||||
}
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::Save(const Core::CPUThreadGuard& guard, std::FILE* file) const
|
||||
{
|
||||
m_branch_watch.Save(guard, file);
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::Load(const Core::CPUThreadGuard& guard, std::FILE* file)
|
||||
{
|
||||
emit layoutAboutToBeChanged();
|
||||
m_branch_watch.Load(guard, file);
|
||||
PrefetchSymbols();
|
||||
emit layoutChanged();
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::UpdateSymbols()
|
||||
{
|
||||
const int row_count = rowCount();
|
||||
if (row_count <= 0)
|
||||
return;
|
||||
static const QList<int> roles = {Qt::DisplayRole};
|
||||
PrefetchSymbols();
|
||||
const int last = row_count - 1;
|
||||
emit dataChanged(createIndex(0, Column::OriginSymbol), createIndex(last, Column::DestinSymbol),
|
||||
roles);
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::UpdateHits()
|
||||
{
|
||||
const int row_count = rowCount();
|
||||
if (row_count <= 0)
|
||||
return;
|
||||
static const QList<int> roles = {Qt::DisplayRole};
|
||||
const int last = row_count - 1;
|
||||
emit dataChanged(createIndex(0, Column::RecentHits), createIndex(last, Column::TotalHits), roles);
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::SetInspected(const QModelIndex& index)
|
||||
{
|
||||
const int row = index.row();
|
||||
switch (index.column())
|
||||
{
|
||||
case Column::Origin:
|
||||
SetOriginInspected(m_branch_watch.GetSelection()[row].collection_ptr->first.origin_addr);
|
||||
return;
|
||||
case Column::Destination:
|
||||
SetDestinInspected(m_branch_watch.GetSelection()[row].collection_ptr->first.destin_addr, false);
|
||||
return;
|
||||
case Column::OriginSymbol:
|
||||
SetSymbolInspected(m_symbol_list[row].origin_addr.value<u32>(), false);
|
||||
return;
|
||||
case Column::DestinSymbol:
|
||||
SetSymbolInspected(m_symbol_list[row].destin_addr.value<u32>(), false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::SetOriginInspected(u32 origin_addr)
|
||||
{
|
||||
using Inspection = Core::BranchWatchSelectionInspection;
|
||||
static const QList<int> roles = {Qt::FontRole, Qt::ForegroundRole};
|
||||
|
||||
const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection();
|
||||
for (std::size_t i = 0; i < selection.size(); ++i)
|
||||
{
|
||||
if (selection[i].collection_ptr->first.origin_addr != origin_addr)
|
||||
continue;
|
||||
m_branch_watch.SetSelectedInspected(i, Inspection::SetOriginNOP);
|
||||
const QModelIndex index = createIndex(static_cast<int>(i), Column::Origin);
|
||||
emit dataChanged(index, index, roles);
|
||||
}
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::SetDestinInspected(u32 destin_addr, bool nested)
|
||||
{
|
||||
using Inspection = Core::BranchWatchSelectionInspection;
|
||||
static const QList<int> roles = {Qt::FontRole, Qt::ForegroundRole};
|
||||
|
||||
const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection();
|
||||
for (std::size_t i = 0; i < selection.size(); ++i)
|
||||
{
|
||||
if (selection[i].collection_ptr->first.destin_addr != destin_addr)
|
||||
continue;
|
||||
m_branch_watch.SetSelectedInspected(i, Inspection::SetDestinBLR);
|
||||
const QModelIndex index = createIndex(static_cast<int>(i), Column::Destination);
|
||||
emit dataChanged(index, index, roles);
|
||||
}
|
||||
|
||||
if (nested)
|
||||
return;
|
||||
SetSymbolInspected(destin_addr, true);
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::SetSymbolInspected(u32 symbol_addr, bool nested)
|
||||
{
|
||||
using Inspection = Core::BranchWatchSelectionInspection;
|
||||
static const QList<int> roles = {Qt::FontRole, Qt::ForegroundRole};
|
||||
|
||||
for (qsizetype i = 0; i < m_symbol_list.size(); ++i)
|
||||
{
|
||||
const SymbolListValueType& value = m_symbol_list[i];
|
||||
if (value.origin_addr.isValid() && value.origin_addr.value<u32>() == symbol_addr)
|
||||
{
|
||||
m_branch_watch.SetSelectedInspected(i, Inspection::SetOriginSymbolBLR);
|
||||
const QModelIndex index = createIndex(i, Column::OriginSymbol);
|
||||
emit dataChanged(index, index, roles);
|
||||
}
|
||||
if (value.destin_addr.isValid() && value.destin_addr.value<u32>() == symbol_addr)
|
||||
{
|
||||
m_branch_watch.SetSelectedInspected(i, Inspection::SetDestinSymbolBLR);
|
||||
const QModelIndex index = createIndex(i, Column::DestinSymbol);
|
||||
emit dataChanged(index, index, roles);
|
||||
}
|
||||
}
|
||||
|
||||
if (nested)
|
||||
return;
|
||||
SetDestinInspected(symbol_addr, true);
|
||||
}
|
||||
|
||||
void BranchWatchTableModel::PrefetchSymbols()
|
||||
{
|
||||
if (m_branch_watch.GetRecordingPhase() != Core::BranchWatch::Phase::Reduction)
|
||||
return;
|
||||
|
||||
const Core::BranchWatch::Selection& selection = m_branch_watch.GetSelection();
|
||||
m_symbol_list.clear();
|
||||
m_symbol_list.reserve(selection.size());
|
||||
for (const Core::BranchWatch::Selection::value_type& value : selection)
|
||||
{
|
||||
const Core::BranchWatch::Collection::value_type* const kv = value.collection_ptr;
|
||||
m_symbol_list.emplace_back(g_symbolDB.GetSymbolFromAddr(kv->first.origin_addr),
|
||||
g_symbolDB.GetSymbolFromAddr(kv->first.destin_addr));
|
||||
}
|
||||
}
|
||||
|
||||
static QVariant GetValidSymbolStringVariant(const QVariant& symbol_name_v)
|
||||
{
|
||||
if (symbol_name_v.isValid())
|
||||
return symbol_name_v;
|
||||
return QStringLiteral(" --- ");
|
||||
}
|
||||
|
||||
static QString GetInstructionMnemonic(u32 hex)
|
||||
{
|
||||
const std::string disas = Common::GekkoDisassembler::Disassemble(hex, 0);
|
||||
const std::string::size_type split = disas.find('\t');
|
||||
// I wish I could disassemble just the mnemonic!
|
||||
if (split == std::string::npos)
|
||||
return QString::fromStdString(disas);
|
||||
return QString::fromLatin1(disas.data(), split);
|
||||
}
|
||||
|
||||
static bool BranchIsUnconditional(UGeckoInstruction inst)
|
||||
{
|
||||
if (inst.OPCD == 18) // bx
|
||||
return true;
|
||||
// If BranchWatch is doing its job, the input will be only bcx, bclrx, and bcctrx instructions.
|
||||
DEBUG_ASSERT(inst.OPCD == 16 || (inst.OPCD == 19 && (inst.SUBOP10 == 16 || inst.SUBOP10 == 528)));
|
||||
if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static QString GetConditionString(const Core::BranchWatch::Selection::value_type& value,
|
||||
const Core::BranchWatch::Collection::value_type* kv)
|
||||
{
|
||||
if (value.condition == false)
|
||||
return BranchWatchTableModel::tr("false");
|
||||
if (BranchIsUnconditional(kv->first.original_inst))
|
||||
return QStringLiteral("");
|
||||
return BranchWatchTableModel::tr("true");
|
||||
}
|
||||
|
||||
QVariant BranchWatchTableModel::DisplayRoleData(const QModelIndex& index) const
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case Column::OriginSymbol:
|
||||
return GetValidSymbolStringVariant(m_symbol_list[index.row()].origin_name);
|
||||
case Column::DestinSymbol:
|
||||
return GetValidSymbolStringVariant(m_symbol_list[index.row()].destin_name);
|
||||
}
|
||||
const Core::BranchWatch::Selection::value_type& value =
|
||||
m_branch_watch.GetSelection()[index.row()];
|
||||
const Core::BranchWatch::Collection::value_type* kv = value.collection_ptr;
|
||||
switch (index.column())
|
||||
{
|
||||
case Column::Instruction:
|
||||
return GetInstructionMnemonic(kv->first.original_inst.hex);
|
||||
case Column::Condition:
|
||||
return GetConditionString(value, kv);
|
||||
case Column::Origin:
|
||||
return QString::number(kv->first.origin_addr, 16);
|
||||
case Column::Destination:
|
||||
return QString::number(kv->first.destin_addr, 16);
|
||||
case Column::RecentHits:
|
||||
return QString::number(kv->second.total_hits - kv->second.hits_snapshot);
|
||||
case Column::TotalHits:
|
||||
return QString::number(kv->second.total_hits);
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant BranchWatchTableModel::FontRoleData(const QModelIndex& index) const
|
||||
{
|
||||
m_font.setBold([&]() -> bool {
|
||||
switch (index.column())
|
||||
{
|
||||
using Inspection = Core::BranchWatchSelectionInspection;
|
||||
case Column::Origin:
|
||||
return (m_branch_watch.GetSelection()[index.row()].inspection & Inspection::SetOriginNOP) !=
|
||||
Inspection{};
|
||||
case Column::Destination:
|
||||
return (m_branch_watch.GetSelection()[index.row()].inspection & Inspection::SetDestinBLR) !=
|
||||
Inspection{};
|
||||
case Column::OriginSymbol:
|
||||
return (m_branch_watch.GetSelection()[index.row()].inspection &
|
||||
Inspection::SetOriginSymbolBLR) != Inspection{};
|
||||
case Column::DestinSymbol:
|
||||
return (m_branch_watch.GetSelection()[index.row()].inspection &
|
||||
Inspection::SetDestinSymbolBLR) != Inspection{};
|
||||
}
|
||||
// Importantly, this code path avoids subscripting the selection to get an inspection value.
|
||||
return false;
|
||||
}());
|
||||
return m_font;
|
||||
}
|
||||
|
||||
QVariant BranchWatchTableModel::TextAlignmentRoleData(const QModelIndex& index) const
|
||||
{
|
||||
// Qt enums become QFlags when operators are used. QVariant's constructors don't support QFlags.
|
||||
switch (index.column())
|
||||
{
|
||||
case Column::Condition:
|
||||
case Column::Origin:
|
||||
case Column::Destination:
|
||||
return Qt::AlignCenter;
|
||||
case Column::RecentHits:
|
||||
case Column::TotalHits:
|
||||
return QVariant::fromValue(Qt::AlignRight | Qt::AlignVCenter);
|
||||
case Column::Instruction:
|
||||
case Column::OriginSymbol:
|
||||
case Column::DestinSymbol:
|
||||
return QVariant::fromValue(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant BranchWatchTableModel::ForegroundRoleData(const QModelIndex& index) const
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
using Inspection = Core::BranchWatchSelectionInspection;
|
||||
case Column::Origin:
|
||||
{
|
||||
const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection;
|
||||
return (inspection & Inspection::SetOriginNOP) != Inspection{} ? QBrush(Qt::red) : QVariant();
|
||||
}
|
||||
case Column::Destination:
|
||||
{
|
||||
const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection;
|
||||
return (inspection & Inspection::SetDestinBLR) != Inspection{} ? QBrush(Qt::red) : QVariant();
|
||||
}
|
||||
case Column::OriginSymbol:
|
||||
{
|
||||
const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection;
|
||||
return (inspection & Inspection::SetOriginSymbolBLR) != Inspection{} ? QBrush(Qt::red) :
|
||||
QVariant();
|
||||
}
|
||||
case Column::DestinSymbol:
|
||||
{
|
||||
const Inspection inspection = m_branch_watch.GetSelection()[index.row()].inspection;
|
||||
return (inspection & Inspection::SetDestinSymbolBLR) != Inspection{} ? QBrush(Qt::red) :
|
||||
QVariant();
|
||||
}
|
||||
}
|
||||
// Importantly, this code path avoids subscripting the selection to get an inspection value.
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant BranchWatchTableModel::ClickRoleData(const QModelIndex& index) const
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case Column::OriginSymbol:
|
||||
return m_symbol_list[index.row()].origin_addr;
|
||||
case Column::DestinSymbol:
|
||||
return m_symbol_list[index.row()].destin_addr;
|
||||
}
|
||||
const Core::BranchWatch::Collection::value_type* kv =
|
||||
m_branch_watch.GetSelection()[index.row()].collection_ptr;
|
||||
switch (index.column())
|
||||
{
|
||||
case Column::Instruction:
|
||||
return kv->first.original_inst.hex;
|
||||
case Column::Origin:
|
||||
return kv->first.origin_addr;
|
||||
case Column::Destination:
|
||||
return kv->first.destin_addr;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
// 0 == false, 1 == true, 2 == unconditional
|
||||
static int GetConditionInteger(const Core::BranchWatch::Selection::value_type& value,
|
||||
const Core::BranchWatch::Collection::value_type* kv)
|
||||
{
|
||||
if (value.condition == false)
|
||||
return 0;
|
||||
if (BranchIsUnconditional(kv->first.original_inst))
|
||||
return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant BranchWatchTableModel::SortRoleData(const QModelIndex& index) const
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case Column::OriginSymbol:
|
||||
return m_symbol_list[index.row()].origin_name;
|
||||
case Column::DestinSymbol:
|
||||
return m_symbol_list[index.row()].destin_name;
|
||||
}
|
||||
const Core::BranchWatch::Selection::value_type& selection_value =
|
||||
m_branch_watch.GetSelection()[index.row()];
|
||||
const Core::BranchWatch::Collection::value_type* kv = selection_value.collection_ptr;
|
||||
switch (index.column())
|
||||
{
|
||||
// QVariant's ctor only supports (unsigned) int and (unsigned) long long for some stupid reason.
|
||||
// std::size_t is unsigned long on some platforms, which results in an ambiguous conversion.
|
||||
case Column::Instruction:
|
||||
return GetInstructionMnemonic(kv->first.original_inst.hex);
|
||||
case Column::Condition:
|
||||
return GetConditionInteger(selection_value, kv);
|
||||
case Column::Origin:
|
||||
return kv->first.origin_addr;
|
||||
case Column::Destination:
|
||||
return kv->first.destin_addr;
|
||||
case Column::RecentHits:
|
||||
return qulonglong{kv->second.total_hits - kv->second.hits_snapshot};
|
||||
case Column::TotalHits:
|
||||
return qulonglong{kv->second.total_hits};
|
||||
}
|
||||
return QVariant();
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include <QAbstractTableModel>
|
||||
#include <QFont>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
|
||||
#include "Common/SymbolDB.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class BranchWatch;
|
||||
class CPUThreadGuard;
|
||||
class System;
|
||||
} // namespace Core
|
||||
|
||||
namespace BranchWatchTableModelColumn
|
||||
{
|
||||
enum EnumType : int
|
||||
{
|
||||
Instruction = 0,
|
||||
Condition,
|
||||
Origin,
|
||||
Destination,
|
||||
RecentHits,
|
||||
TotalHits,
|
||||
OriginSymbol,
|
||||
DestinSymbol,
|
||||
NumberOfColumns,
|
||||
};
|
||||
}
|
||||
|
||||
namespace BranchWatchTableModelUserRole
|
||||
{
|
||||
enum EnumType : int
|
||||
{
|
||||
ClickRole = Qt::UserRole,
|
||||
SortRole,
|
||||
};
|
||||
}
|
||||
|
||||
struct BranchWatchTableModelSymbolListValueType
|
||||
{
|
||||
explicit BranchWatchTableModelSymbolListValueType(const Common::Symbol* const origin_symbol,
|
||||
const Common::Symbol* const destin_symbol)
|
||||
: origin_name(origin_symbol ? QString::fromStdString(origin_symbol->name) : QVariant{}),
|
||||
origin_addr(origin_symbol ? origin_symbol->address : QVariant{}),
|
||||
destin_name(destin_symbol ? QString::fromStdString(destin_symbol->name) : QVariant{}),
|
||||
destin_addr(destin_symbol ? destin_symbol->address : QVariant{})
|
||||
{
|
||||
}
|
||||
QVariant origin_name, origin_addr;
|
||||
QVariant destin_name, destin_addr;
|
||||
};
|
||||
|
||||
class BranchWatchTableModel final : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using Column = BranchWatchTableModelColumn::EnumType;
|
||||
using UserRole = BranchWatchTableModelUserRole::EnumType;
|
||||
using SymbolListValueType = BranchWatchTableModelSymbolListValueType;
|
||||
using SymbolList = QList<SymbolListValueType>;
|
||||
|
||||
explicit BranchWatchTableModel(Core::System& system, Core::BranchWatch& branch_watch,
|
||||
QObject* parent = nullptr)
|
||||
: QAbstractTableModel(parent), m_system(system), m_branch_watch(branch_watch)
|
||||
{
|
||||
}
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation,
|
||||
int role = Qt::DisplayRole) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex{}) const override;
|
||||
int columnCount(const QModelIndex& parent = QModelIndex{}) const override;
|
||||
bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex{}) override;
|
||||
void setFont(const QFont& font) { m_font = font; }
|
||||
|
||||
void OnClearBranchWatch(const Core::CPUThreadGuard& guard);
|
||||
void OnCodePathWasTaken(const Core::CPUThreadGuard& guard);
|
||||
void OnCodePathNotTaken(const Core::CPUThreadGuard& guard);
|
||||
void OnBranchWasOverwritten(const Core::CPUThreadGuard& guard);
|
||||
void OnBranchNotOverwritten(const Core::CPUThreadGuard& guard);
|
||||
void OnWipeRecentHits();
|
||||
void OnWipeInspection();
|
||||
void OnDelete(QModelIndexList index_list);
|
||||
|
||||
void Save(const Core::CPUThreadGuard& guard, std::FILE* file) const;
|
||||
void Load(const Core::CPUThreadGuard& guard, std::FILE* file);
|
||||
void UpdateSymbols();
|
||||
void UpdateHits();
|
||||
void SetInspected(const QModelIndex& index);
|
||||
|
||||
const SymbolList& GetSymbolList() const { return m_symbol_list; }
|
||||
|
||||
private:
|
||||
void SetOriginInspected(u32 origin_addr);
|
||||
void SetDestinInspected(u32 destin_addr, bool nested);
|
||||
void SetSymbolInspected(u32 symbol_addr, bool nested);
|
||||
void PrefetchSymbols();
|
||||
|
||||
[[nodiscard]] QVariant DisplayRoleData(const QModelIndex& index) const;
|
||||
[[nodiscard]] QVariant FontRoleData(const QModelIndex& index) const;
|
||||
[[nodiscard]] QVariant TextAlignmentRoleData(const QModelIndex& index) const;
|
||||
[[nodiscard]] QVariant ForegroundRoleData(const QModelIndex& index) const;
|
||||
[[nodiscard]] QVariant ClickRoleData(const QModelIndex& index) const;
|
||||
[[nodiscard]] QVariant SortRoleData(const QModelIndex& index) const;
|
||||
|
||||
Core::System& m_system;
|
||||
Core::BranchWatch& m_branch_watch;
|
||||
|
||||
SymbolList m_symbol_list;
|
||||
mutable QFont m_font;
|
||||
};
|
|
@ -1,673 +0,0 @@
|
|||
// Copyright 2022 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/Debugger/CodeDiffDialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QStyleHints>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Common/FileUtil.h"
|
||||
#include "Common/IOFile.h"
|
||||
#include "Common/MsgHandler.h"
|
||||
#include "Common/StringUtil.h"
|
||||
#include "Core/ConfigManager.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/Debugger/PPCDebugInterface.h"
|
||||
#include "Core/HW/CPU.h"
|
||||
#include "Core/PowerPC/JitInterface.h"
|
||||
#include "Core/PowerPC/MMU.h"
|
||||
#include "Core/PowerPC/PPCSymbolDB.h"
|
||||
#include "Core/PowerPC/PowerPC.h"
|
||||
#include "Core/PowerPC/Profiler.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
#include "DolphinQt/Debugger/CodeWidget.h"
|
||||
#include "DolphinQt/Host.h"
|
||||
#include "DolphinQt/QtUtils/ModalMessageBox.h"
|
||||
#include "DolphinQt/Settings.h"
|
||||
|
||||
static const QString RECORD_BUTTON_STYLESHEET = QStringLiteral(
|
||||
"QPushButton:checked { background-color: rgb(150, 0, 0); border-style: solid;"
|
||||
"padding: 0px; border-width: 3px; border-color: rgb(150,0,0); color: rgb(255, 255, 255);}");
|
||||
|
||||
CodeDiffDialog::CodeDiffDialog(CodeWidget* parent) : QDialog(parent), m_code_widget(parent)
|
||||
{
|
||||
setWindowTitle(tr("Code Diff Tool"));
|
||||
CreateWidgets();
|
||||
auto& settings = Settings::GetQSettings();
|
||||
restoreGeometry(settings.value(QStringLiteral("diffdialog/geometry")).toByteArray());
|
||||
ConnectWidgets();
|
||||
}
|
||||
|
||||
void CodeDiffDialog::reject()
|
||||
{
|
||||
ClearData();
|
||||
auto& settings = Settings::GetQSettings();
|
||||
settings.setValue(QStringLiteral("diffdialog/geometry"), saveGeometry());
|
||||
QDialog::reject();
|
||||
}
|
||||
|
||||
void CodeDiffDialog::CreateWidgets()
|
||||
{
|
||||
bool running = Core::GetState() != Core::State::Uninitialized;
|
||||
|
||||
auto* btns_layout = new QGridLayout;
|
||||
m_exclude_btn = new QPushButton(tr("Code did not get executed"));
|
||||
m_include_btn = new QPushButton(tr("Code has been executed"));
|
||||
m_record_btn = new QPushButton(tr("Start Recording"));
|
||||
m_record_btn->setCheckable(true);
|
||||
m_record_btn->setStyleSheet(RECORD_BUTTON_STYLESHEET);
|
||||
m_record_btn->setEnabled(running);
|
||||
m_exclude_btn->setEnabled(false);
|
||||
m_include_btn->setEnabled(false);
|
||||
|
||||
btns_layout->addWidget(m_exclude_btn, 0, 0);
|
||||
btns_layout->addWidget(m_include_btn, 0, 1);
|
||||
btns_layout->addWidget(m_record_btn, 0, 2);
|
||||
|
||||
auto* labels_layout = new QHBoxLayout;
|
||||
m_exclude_size_label = new QLabel(tr("Excluded: 0"));
|
||||
m_include_size_label = new QLabel(tr("Included: 0"));
|
||||
|
||||
btns_layout->addWidget(m_exclude_size_label, 1, 0);
|
||||
btns_layout->addWidget(m_include_size_label, 1, 1);
|
||||
|
||||
m_matching_results_table = new QTableWidget();
|
||||
m_matching_results_table->setColumnCount(5);
|
||||
m_matching_results_table->setHorizontalHeaderLabels(
|
||||
{tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")});
|
||||
m_matching_results_table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_matching_results_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_matching_results_table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
m_matching_results_table->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_matching_results_table->setColumnWidth(0, 60);
|
||||
m_matching_results_table->setColumnWidth(1, 60);
|
||||
m_matching_results_table->setColumnWidth(2, 4);
|
||||
m_matching_results_table->setColumnWidth(3, 210);
|
||||
m_matching_results_table->setColumnWidth(4, 65);
|
||||
m_matching_results_table->setCornerButtonEnabled(false);
|
||||
m_autosave_check = new QCheckBox(tr("Auto Save"));
|
||||
m_save_btn = new QPushButton(tr("Save"));
|
||||
m_save_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
m_save_btn->setEnabled(running);
|
||||
m_load_btn = new QPushButton(tr("Load"));
|
||||
m_load_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
m_load_btn->setEnabled(running);
|
||||
m_reset_btn = new QPushButton(tr("Reset All"));
|
||||
m_reset_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
m_help_btn = new QPushButton(tr("Help"));
|
||||
m_help_btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
auto* bottom_controls_layout = new QHBoxLayout;
|
||||
bottom_controls_layout->addWidget(m_reset_btn, 0, Qt::AlignLeft);
|
||||
bottom_controls_layout->addStretch();
|
||||
bottom_controls_layout->addWidget(m_autosave_check, 0, Qt::AlignRight);
|
||||
bottom_controls_layout->addWidget(m_save_btn, 0, Qt::AlignRight);
|
||||
bottom_controls_layout->addWidget(m_load_btn, 0, Qt::AlignRight);
|
||||
bottom_controls_layout->addWidget(m_help_btn, 0, Qt::AlignRight);
|
||||
|
||||
auto* layout = new QVBoxLayout();
|
||||
layout->addLayout(btns_layout);
|
||||
layout->addLayout(labels_layout);
|
||||
layout->addWidget(m_matching_results_table);
|
||||
layout->addLayout(bottom_controls_layout);
|
||||
|
||||
setLayout(layout);
|
||||
resize(515, 400);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::ConnectWidgets()
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, this,
|
||||
[this](Qt::ColorScheme colorScheme) {
|
||||
m_record_btn->setStyleSheet(RECORD_BUTTON_STYLESHEET);
|
||||
});
|
||||
#endif
|
||||
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
|
||||
[this](Core::State state) { UpdateButtons(state != Core::State::Uninitialized); });
|
||||
connect(m_record_btn, &QPushButton::toggled, this, &CodeDiffDialog::OnRecord);
|
||||
connect(m_include_btn, &QPushButton::pressed, [this]() { Update(UpdateType::Include); });
|
||||
connect(m_exclude_btn, &QPushButton::pressed, [this]() { Update(UpdateType::Exclude); });
|
||||
connect(m_matching_results_table, &QTableWidget::itemClicked, [this]() { OnClickItem(); });
|
||||
connect(m_save_btn, &QPushButton::pressed, this, &CodeDiffDialog::SaveDataBackup);
|
||||
connect(m_load_btn, &QPushButton::pressed, this, &CodeDiffDialog::LoadDataBackup);
|
||||
connect(m_reset_btn, &QPushButton::pressed, this, &CodeDiffDialog::ClearData);
|
||||
connect(m_help_btn, &QPushButton::pressed, this, &CodeDiffDialog::InfoDisp);
|
||||
connect(m_matching_results_table, &CodeDiffDialog::customContextMenuRequested, this,
|
||||
&CodeDiffDialog::OnContextMenu);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::OnClickItem()
|
||||
{
|
||||
UpdateItem();
|
||||
auto address = m_matching_results_table->currentItem()->data(Qt::UserRole).toUInt();
|
||||
m_code_widget->SetAddress(address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::SaveDataBackup()
|
||||
{
|
||||
if (Core::GetState() == Core::State::Uninitialized)
|
||||
{
|
||||
ModalMessageBox::information(this, tr("Code Diff Tool"),
|
||||
tr("Emulation must be started before saving a file."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_include.empty())
|
||||
return;
|
||||
|
||||
std::string filename =
|
||||
File::GetUserPath(D_LOGS_IDX) + SConfig::GetInstance().GetGameID() + "_CodeDiff.txt";
|
||||
File::IOFile f(filename, "w");
|
||||
if (!f)
|
||||
{
|
||||
ModalMessageBox::information(
|
||||
this, tr("Code Diff Tool"),
|
||||
tr("Failed to save file to: %1").arg(QString::fromStdString(filename)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy list of BLR tested functions:
|
||||
std::set<u32> address_blr;
|
||||
for (int i = 0; i < m_matching_results_table->rowCount(); i++)
|
||||
{
|
||||
if (m_matching_results_table->item(i, 4)->text() == QStringLiteral("X"))
|
||||
address_blr.insert(m_matching_results_table->item(i, 4)->data(Qt::UserRole).toUInt());
|
||||
}
|
||||
|
||||
for (const auto& line : m_include)
|
||||
{
|
||||
bool blr = address_blr.contains(line.addr);
|
||||
f.WriteString(
|
||||
fmt::format("{} {} {} {:d} {}\n", line.addr, line.hits, line.total_hits, blr, line.symbol));
|
||||
}
|
||||
}
|
||||
|
||||
void CodeDiffDialog::LoadDataBackup()
|
||||
{
|
||||
if (Core::GetState() == Core::State::Uninitialized)
|
||||
{
|
||||
ModalMessageBox::information(this, tr("Code Diff Tool"),
|
||||
tr("Emulation must be started before loading a file."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_symbolDB.IsEmpty())
|
||||
{
|
||||
ModalMessageBox::warning(
|
||||
this, tr("Code Diff Tool"),
|
||||
tr("Symbol map not found.\n\nIf one does not exist, you can generate one from "
|
||||
"the Menu bar:\nSymbols -> Generate Symbols From ->\n\tAddress | Signature "
|
||||
"Database | RSO Modules"));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string filename =
|
||||
File::GetUserPath(D_LOGS_IDX) + SConfig::GetInstance().GetGameID() + "_CodeDiff.txt";
|
||||
File::IOFile f(filename, "r");
|
||||
if (!f)
|
||||
{
|
||||
ModalMessageBox::information(
|
||||
this, tr("Code Diff Tool"),
|
||||
tr("Failed to find or open file: %1").arg(QString::fromStdString(filename)));
|
||||
return;
|
||||
};
|
||||
|
||||
ClearData();
|
||||
|
||||
std::set<u32> blr_addresses;
|
||||
char line[512];
|
||||
while (fgets(line, 512, f.GetHandle()))
|
||||
{
|
||||
bool blr = false;
|
||||
Diff temp;
|
||||
std::istringstream iss(line);
|
||||
iss.imbue(std::locale::classic());
|
||||
iss >> temp.addr >> temp.hits >> temp.total_hits >> blr >> std::ws;
|
||||
std::getline(iss, temp.symbol);
|
||||
|
||||
if (blr)
|
||||
blr_addresses.insert(temp.addr);
|
||||
|
||||
m_include.push_back(std::move(temp));
|
||||
}
|
||||
|
||||
Update(UpdateType::Backup);
|
||||
|
||||
for (int i = 0; i < m_matching_results_table->rowCount(); i++)
|
||||
{
|
||||
if (blr_addresses.contains(m_matching_results_table->item(i, 4)->data(Qt::UserRole).toUInt()))
|
||||
MarkRowBLR(i);
|
||||
}
|
||||
}
|
||||
|
||||
void CodeDiffDialog::ClearData()
|
||||
{
|
||||
if (m_record_btn->isChecked())
|
||||
m_record_btn->toggle();
|
||||
ClearBlockCache();
|
||||
m_matching_results_table->clear();
|
||||
m_matching_results_table->setRowCount(0);
|
||||
m_matching_results_table->setHorizontalHeaderLabels(
|
||||
{tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")});
|
||||
m_matching_results_table->setEditTriggers(QAbstractItemView::EditTrigger::NoEditTriggers);
|
||||
m_exclude_size_label->setText(tr("Excluded: %1").arg(0));
|
||||
m_include_size_label->setText(tr("Included: %1").arg(0));
|
||||
m_exclude_btn->setEnabled(false);
|
||||
m_include_btn->setEnabled(false);
|
||||
m_include_active = false;
|
||||
// Swap is used instead of clear for efficiency in the case of huge m_include/m_exclude
|
||||
std::vector<Diff>().swap(m_include);
|
||||
std::vector<Diff>().swap(m_exclude);
|
||||
Core::System::GetInstance().GetJitInterface().SetProfilingState(
|
||||
JitInterface::ProfilingState::Disabled);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::ClearBlockCache()
|
||||
{
|
||||
Core::State old_state = Core::GetState();
|
||||
|
||||
if (old_state == Core::State::Running)
|
||||
Core::SetState(Core::State::Paused, false);
|
||||
|
||||
Core::System::GetInstance().GetJitInterface().ClearCache();
|
||||
|
||||
if (old_state == Core::State::Running)
|
||||
Core::SetState(Core::State::Running);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::OnRecord(bool enabled)
|
||||
{
|
||||
if (m_failed_requirements)
|
||||
{
|
||||
m_failed_requirements = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Core::GetState() == Core::State::Uninitialized)
|
||||
{
|
||||
ModalMessageBox::information(this, tr("Code Diff Tool"),
|
||||
tr("Emulation must be started to record."));
|
||||
m_failed_requirements = true;
|
||||
m_record_btn->setChecked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_symbolDB.IsEmpty())
|
||||
{
|
||||
ModalMessageBox::warning(
|
||||
this, tr("Code Diff Tool"),
|
||||
tr("Symbol map not found.\n\nIf one does not exist, you can generate one from "
|
||||
"the Menu bar:\nSymbols -> Generate Symbols From ->\n\tAddress | Signature "
|
||||
"Database | RSO Modules"));
|
||||
m_failed_requirements = true;
|
||||
m_record_btn->setChecked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
JitInterface::ProfilingState state;
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
ClearBlockCache();
|
||||
m_record_btn->setText(tr("Stop Recording"));
|
||||
state = JitInterface::ProfilingState::Enabled;
|
||||
m_exclude_btn->setEnabled(true);
|
||||
m_include_btn->setEnabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearBlockCache();
|
||||
m_record_btn->setText(tr("Start Recording"));
|
||||
state = JitInterface::ProfilingState::Disabled;
|
||||
m_exclude_btn->setEnabled(false);
|
||||
m_include_btn->setEnabled(false);
|
||||
}
|
||||
|
||||
m_record_btn->update();
|
||||
Core::System::GetInstance().GetJitInterface().SetProfilingState(state);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::OnInclude()
|
||||
{
|
||||
const auto recorded_symbols = CalculateSymbolsFromProfile();
|
||||
|
||||
if (recorded_symbols.empty())
|
||||
return;
|
||||
|
||||
if (m_include.empty() && m_exclude.empty())
|
||||
{
|
||||
m_include = recorded_symbols;
|
||||
m_include_active = true;
|
||||
}
|
||||
else if (m_include.empty())
|
||||
{
|
||||
// If include becomes empty after having items on it, don't refill it until after a reset.
|
||||
if (m_include_active)
|
||||
return;
|
||||
|
||||
// If we are building include for the first time and we have an exlcude list, then include =
|
||||
// recorded - excluded.
|
||||
m_include = recorded_symbols;
|
||||
RemoveMatchingSymbolsFromIncludes(m_exclude);
|
||||
m_include_active = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If include already exists, keep items that are in both include and recorded. Exclude list
|
||||
// becomes irrelevant.
|
||||
RemoveMissingSymbolsFromIncludes(recorded_symbols);
|
||||
}
|
||||
}
|
||||
|
||||
void CodeDiffDialog::OnExclude()
|
||||
{
|
||||
const auto recorded_symbols = CalculateSymbolsFromProfile();
|
||||
if (m_include.empty() && m_exclude.empty())
|
||||
{
|
||||
m_exclude = recorded_symbols;
|
||||
}
|
||||
else if (m_include.empty())
|
||||
{
|
||||
// If there is only an exclude list, update it.
|
||||
for (auto& iter : recorded_symbols)
|
||||
{
|
||||
auto pos = std::lower_bound(m_exclude.begin(), m_exclude.end(), iter.symbol);
|
||||
|
||||
if (pos == m_exclude.end() || pos->symbol != iter.symbol)
|
||||
m_exclude.insert(pos, iter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If include already exists, the exclude list will have been used to trim it, so the exclude
|
||||
// list is now irrelevant, as anythng not on the include list is effectively excluded.
|
||||
// Exclude/subtract recorded items from the include list.
|
||||
RemoveMatchingSymbolsFromIncludes(recorded_symbols);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Diff> CodeDiffDialog::CalculateSymbolsFromProfile() const
|
||||
{
|
||||
Profiler::ProfileStats prof_stats;
|
||||
auto& blockstats = prof_stats.block_stats;
|
||||
Core::System::GetInstance().GetJitInterface().GetProfileResults(&prof_stats);
|
||||
std::vector<Diff> current;
|
||||
current.reserve(20000);
|
||||
|
||||
// Convert blockstats to smaller struct Diff. Exclude repeat functions via symbols.
|
||||
for (const auto& iter : blockstats)
|
||||
{
|
||||
std::string symbol = g_symbolDB.GetDescription(iter.addr);
|
||||
if (!std::any_of(current.begin(), current.end(),
|
||||
[&symbol](const Diff& v) { return v.symbol == symbol; }))
|
||||
{
|
||||
current.push_back(Diff{
|
||||
.addr = iter.addr,
|
||||
.symbol = std::move(symbol),
|
||||
.hits = static_cast<u32>(iter.run_count),
|
||||
.total_hits = static_cast<u32>(iter.run_count),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(current.begin(), current.end(),
|
||||
[](const Diff& v1, const Diff& v2) { return (v1.symbol < v2.symbol); });
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
void CodeDiffDialog::RemoveMissingSymbolsFromIncludes(const std::vector<Diff>& symbol_diff)
|
||||
{
|
||||
m_include.erase(std::remove_if(m_include.begin(), m_include.end(),
|
||||
[&](const Diff& v) {
|
||||
auto arg = std::none_of(
|
||||
symbol_diff.begin(), symbol_diff.end(), [&](const Diff& p) {
|
||||
return p.symbol == v.symbol || p.addr == v.addr;
|
||||
});
|
||||
return arg;
|
||||
}),
|
||||
m_include.end());
|
||||
for (auto& original_includes : m_include)
|
||||
{
|
||||
auto pos = std::lower_bound(symbol_diff.begin(), symbol_diff.end(), original_includes.symbol);
|
||||
if (pos != symbol_diff.end() &&
|
||||
(pos->symbol == original_includes.symbol || pos->addr == original_includes.addr))
|
||||
{
|
||||
original_includes.total_hits += pos->hits;
|
||||
original_includes.hits = pos->hits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CodeDiffDialog::RemoveMatchingSymbolsFromIncludes(const std::vector<Diff>& symbol_list)
|
||||
{
|
||||
m_include.erase(std::remove_if(m_include.begin(), m_include.end(),
|
||||
[&](const Diff& i) {
|
||||
return std::any_of(
|
||||
symbol_list.begin(), symbol_list.end(), [&](const Diff& s) {
|
||||
return i.symbol == s.symbol || i.addr == s.addr;
|
||||
});
|
||||
}),
|
||||
m_include.end());
|
||||
}
|
||||
|
||||
void CodeDiffDialog::Update(UpdateType type)
|
||||
{
|
||||
// Wrap everything in a pause
|
||||
Core::State old_state = Core::GetState();
|
||||
if (old_state == Core::State::Running)
|
||||
Core::SetState(Core::State::Paused, false);
|
||||
|
||||
// Main process
|
||||
if (type == UpdateType::Include)
|
||||
{
|
||||
OnInclude();
|
||||
}
|
||||
else if (type == UpdateType::Exclude)
|
||||
{
|
||||
OnExclude();
|
||||
}
|
||||
|
||||
if (type != UpdateType::Backup && m_autosave_check->isChecked() && !m_include.empty())
|
||||
SaveDataBackup();
|
||||
|
||||
const auto create_item = [](const QString& string = {}, const u32 address = 0x00000000) {
|
||||
QTableWidgetItem* item = new QTableWidgetItem(string);
|
||||
item->setData(Qt::UserRole, address);
|
||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||
return item;
|
||||
};
|
||||
|
||||
int i = 0;
|
||||
m_matching_results_table->clear();
|
||||
m_matching_results_table->setRowCount(i);
|
||||
m_matching_results_table->setHorizontalHeaderLabels(
|
||||
{tr("Address"), tr("Total Hits"), tr("Hits"), tr("Symbol"), tr("Inspected")});
|
||||
|
||||
for (auto& iter : m_include)
|
||||
{
|
||||
m_matching_results_table->setRowCount(i + 1);
|
||||
|
||||
QString fix_sym = QString::fromStdString(iter.symbol);
|
||||
fix_sym.replace(QStringLiteral("\t"), QStringLiteral(" "));
|
||||
|
||||
m_matching_results_table->setItem(
|
||||
i, 0, create_item(QStringLiteral("%1").arg(iter.addr, 1, 16), iter.addr));
|
||||
m_matching_results_table->setItem(
|
||||
i, 1, create_item(QStringLiteral("%1").arg(iter.total_hits), iter.addr));
|
||||
m_matching_results_table->setItem(i, 2,
|
||||
create_item(QStringLiteral("%1").arg(iter.hits), iter.addr));
|
||||
m_matching_results_table->setItem(i, 3,
|
||||
create_item(QStringLiteral("%1").arg(fix_sym), iter.addr));
|
||||
m_matching_results_table->setItem(i, 4, create_item(QStringLiteral(""), iter.addr));
|
||||
i++;
|
||||
}
|
||||
|
||||
// If we have ruled out all functions from being included.
|
||||
if (m_include_active && m_include.empty())
|
||||
{
|
||||
m_matching_results_table->setRowCount(1);
|
||||
m_matching_results_table->setItem(0, 3, create_item(tr("No possible functions left. Reset.")));
|
||||
}
|
||||
|
||||
m_exclude_size_label->setText(tr("Excluded: %1").arg(m_exclude.size()));
|
||||
m_include_size_label->setText(tr("Included: %1").arg(m_include.size()));
|
||||
|
||||
Core::System::GetInstance().GetJitInterface().ClearCache();
|
||||
if (old_state == Core::State::Running)
|
||||
Core::SetState(Core::State::Running);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::InfoDisp()
|
||||
{
|
||||
ModalMessageBox::information(
|
||||
this, tr("Code Diff Tool Help"),
|
||||
tr("Used to find functions based on when they should be running.\nSimilar to Cheat Engine "
|
||||
"Ultimap.\n"
|
||||
"A symbol map must be loaded prior to use.\n"
|
||||
"Include/Exclude lists will persist on ending/restarting emulation.\nThese lists "
|
||||
"will not persist on Dolphin close."
|
||||
"\n\n'Start Recording': "
|
||||
"keeps track of what functions run.\n'Stop Recording': erases current "
|
||||
"recording without any change to the lists.\n'Code did not get executed': click while "
|
||||
"recording, will add recorded functions to an exclude "
|
||||
"list, then reset the recording list.\n'Code has been executed': click while recording, "
|
||||
"will add recorded function to an include list, then reset the recording list.\n\nAfter "
|
||||
"you use "
|
||||
"both exclude and include once, the exclude list will be subtracted from the include "
|
||||
"list "
|
||||
"and any includes left over will be displayed.\nYou can continue to use "
|
||||
"'Code did not get executed'/'Code has been executed' to narrow down the "
|
||||
"results.\n\n"
|
||||
"Saving will store the current list in Dolphin's Log folder (File -> Open User "
|
||||
"Folder)"));
|
||||
ModalMessageBox::information(
|
||||
this, tr("Code Diff Tool Help"),
|
||||
tr("Example:\n"
|
||||
"You want to find a function that runs when HP is modified.\n1. Start recording and "
|
||||
"play the game without letting HP be modified, then press 'Code did not get "
|
||||
"executed'.\n2. Immediately gain/lose HP and press 'Code has been executed'.\n3. Repeat "
|
||||
"1 or 2 to "
|
||||
"narrow down the results.\nIncludes (Code has been executed) should "
|
||||
"have short recordings focusing on what you want.\n\nPressing 'Code has been "
|
||||
"executed' twice will only keep functions that ran for both recordings. Hits will update "
|
||||
"to reflect the last recording's "
|
||||
"number of Hits. Total Hits will reflect the total number of "
|
||||
"times a function has been executed until the lists are cleared with Reset.\n\nRight "
|
||||
"click -> 'Set blr' will place a "
|
||||
"blr at the top of the symbol.\n"));
|
||||
}
|
||||
|
||||
void CodeDiffDialog::OnContextMenu()
|
||||
{
|
||||
if (m_matching_results_table->currentItem() == nullptr)
|
||||
return;
|
||||
UpdateItem();
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->addAction(tr("&Go to start of function"), this, &CodeDiffDialog::OnGoTop);
|
||||
menu->addAction(tr("Set &blr"), this, &CodeDiffDialog::OnSetBLR);
|
||||
menu->addAction(tr("&Delete"), this, &CodeDiffDialog::OnDelete);
|
||||
menu->exec(QCursor::pos());
|
||||
}
|
||||
|
||||
void CodeDiffDialog::OnGoTop()
|
||||
{
|
||||
auto item = m_matching_results_table->currentItem();
|
||||
if (!item)
|
||||
return;
|
||||
Common::Symbol* symbol = g_symbolDB.GetSymbolFromAddr(item->data(Qt::UserRole).toUInt());
|
||||
if (!symbol)
|
||||
return;
|
||||
m_code_widget->SetAddress(symbol->address, CodeViewWidget::SetAddressUpdate::WithDetailedUpdate);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::OnDelete()
|
||||
{
|
||||
// Delete from include list and qtable widget
|
||||
auto item = m_matching_results_table->currentItem();
|
||||
if (!item)
|
||||
return;
|
||||
int row = m_matching_results_table->row(item);
|
||||
if (row == -1)
|
||||
return;
|
||||
// TODO: If/when sorting is ever added, .erase needs to find item position instead; leaving as is
|
||||
// for performance
|
||||
if (!m_include.empty())
|
||||
{
|
||||
m_include.erase(m_include.begin() + row);
|
||||
}
|
||||
m_matching_results_table->removeRow(row);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::OnSetBLR()
|
||||
{
|
||||
auto item = m_matching_results_table->currentItem();
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
Common::Symbol* symbol = g_symbolDB.GetSymbolFromAddr(item->data(Qt::UserRole).toUInt());
|
||||
if (!symbol)
|
||||
return;
|
||||
|
||||
MarkRowBLR(item->row());
|
||||
if (m_autosave_check->isChecked())
|
||||
SaveDataBackup();
|
||||
|
||||
{
|
||||
auto& system = Core::System::GetInstance();
|
||||
Core::CPUThreadGuard guard(system);
|
||||
system.GetPowerPC().GetDebugInterface().SetPatch(guard, symbol->address, 0x4E800020);
|
||||
}
|
||||
|
||||
m_code_widget->Update();
|
||||
}
|
||||
|
||||
void CodeDiffDialog::MarkRowBLR(int row)
|
||||
{
|
||||
m_matching_results_table->item(row, 0)->setForeground(QBrush(Qt::red));
|
||||
m_matching_results_table->item(row, 1)->setForeground(QBrush(Qt::red));
|
||||
m_matching_results_table->item(row, 2)->setForeground(QBrush(Qt::red));
|
||||
m_matching_results_table->item(row, 3)->setForeground(QBrush(Qt::red));
|
||||
m_matching_results_table->item(row, 4)->setForeground(QBrush(Qt::red));
|
||||
m_matching_results_table->item(row, 4)->setText(QStringLiteral("X"));
|
||||
}
|
||||
|
||||
void CodeDiffDialog::UpdateItem()
|
||||
{
|
||||
QTableWidgetItem* item = m_matching_results_table->currentItem();
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
int row = m_matching_results_table->row(item);
|
||||
if (row == -1)
|
||||
return;
|
||||
uint address = item->data(Qt::UserRole).toUInt();
|
||||
|
||||
auto symbolName = g_symbolDB.GetDescription(address);
|
||||
if (symbolName == " --- ")
|
||||
return;
|
||||
|
||||
QString newName =
|
||||
QString::fromStdString(symbolName).replace(QStringLiteral("\t"), QStringLiteral(" "));
|
||||
m_matching_results_table->item(row, 3)->setText(newName);
|
||||
}
|
||||
|
||||
void CodeDiffDialog::UpdateButtons(bool running)
|
||||
{
|
||||
m_save_btn->setEnabled(running);
|
||||
m_load_btn->setEnabled(running);
|
||||
m_record_btn->setEnabled(running);
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
// Copyright 2022 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
class CodeWidget;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QCheckBox;
|
||||
class QTableWidget;
|
||||
|
||||
struct Diff
|
||||
{
|
||||
u32 addr = 0;
|
||||
std::string symbol;
|
||||
u32 hits = 0;
|
||||
u32 total_hits = 0;
|
||||
|
||||
bool operator<(const std::string& val) const { return symbol < val; }
|
||||
};
|
||||
|
||||
class CodeDiffDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CodeDiffDialog(CodeWidget* parent);
|
||||
void reject() override;
|
||||
|
||||
private:
|
||||
enum class UpdateType
|
||||
{
|
||||
Include,
|
||||
Exclude,
|
||||
Backup
|
||||
};
|
||||
|
||||
void CreateWidgets();
|
||||
void ConnectWidgets();
|
||||
void SaveDataBackup();
|
||||
void LoadDataBackup();
|
||||
void ClearData();
|
||||
void ClearBlockCache();
|
||||
void OnClickItem();
|
||||
void OnRecord(bool enabled);
|
||||
std::vector<Diff> CalculateSymbolsFromProfile() const;
|
||||
void OnInclude();
|
||||
void OnExclude();
|
||||
void RemoveMissingSymbolsFromIncludes(const std::vector<Diff>& symbol_diff);
|
||||
void RemoveMatchingSymbolsFromIncludes(const std::vector<Diff>& symbol_list);
|
||||
void Update(UpdateType type);
|
||||
void InfoDisp();
|
||||
|
||||
void OnContextMenu();
|
||||
|
||||
void OnGoTop();
|
||||
void OnDelete();
|
||||
void OnSetBLR();
|
||||
|
||||
void MarkRowBLR(int row);
|
||||
void UpdateItem();
|
||||
void UpdateButtons(bool running);
|
||||
|
||||
QTableWidget* m_matching_results_table;
|
||||
QCheckBox* m_autosave_check;
|
||||
QLabel* m_exclude_size_label;
|
||||
QLabel* m_include_size_label;
|
||||
QPushButton* m_exclude_btn;
|
||||
QPushButton* m_include_btn;
|
||||
QPushButton* m_record_btn;
|
||||
QPushButton* m_reset_btn;
|
||||
QPushButton* m_save_btn;
|
||||
QPushButton* m_load_btn;
|
||||
QPushButton* m_help_btn;
|
||||
CodeWidget* m_code_widget;
|
||||
|
||||
std::vector<Diff> m_exclude;
|
||||
std::vector<Diff> m_include;
|
||||
bool m_failed_requirements = false;
|
||||
bool m_include_active = false;
|
||||
};
|
|
@ -27,6 +27,7 @@
|
|||
#include "Core/PowerPC/PPCSymbolDB.h"
|
||||
#include "Core/PowerPC/PowerPC.h"
|
||||
#include "Core/System.h"
|
||||
#include "DolphinQt/Debugger/BranchWatchDialog.h"
|
||||
#include "DolphinQt/Host.h"
|
||||
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
|
||||
#include "DolphinQt/Settings.h"
|
||||
|
@ -35,7 +36,10 @@ static const QString BOX_SPLITTER_STYLESHEET = QStringLiteral(
|
|||
"QSplitter::handle { border-top: 1px dashed black; width: 1px; margin-left: 10px; "
|
||||
"margin-right: 10px; }");
|
||||
|
||||
CodeWidget::CodeWidget(QWidget* parent) : QDockWidget(parent), m_system(Core::System::GetInstance())
|
||||
CodeWidget::CodeWidget(QWidget* parent)
|
||||
: QDockWidget(parent), m_system(Core::System::GetInstance()),
|
||||
m_branch_watch_dialog(
|
||||
new BranchWatchDialog(m_system, m_system.GetPowerPC().GetBranchWatch(), this))
|
||||
{
|
||||
setWindowTitle(tr("Code"));
|
||||
setObjectName(QStringLiteral("code"));
|
||||
|
@ -105,7 +109,7 @@ void CodeWidget::CreateWidgets()
|
|||
layout->setSpacing(0);
|
||||
|
||||
m_search_address = new QLineEdit;
|
||||
m_code_diff = new QPushButton(tr("Diff"));
|
||||
m_branch_watch = new QPushButton(tr("Branch Watch"));
|
||||
m_code_view = new CodeViewWidget;
|
||||
|
||||
m_search_address->setPlaceholderText(tr("Search Address"));
|
||||
|
@ -149,7 +153,7 @@ void CodeWidget::CreateWidgets()
|
|||
m_code_splitter->addWidget(m_code_view);
|
||||
|
||||
layout->addWidget(m_search_address, 0, 0);
|
||||
layout->addWidget(m_code_diff, 0, 2);
|
||||
layout->addWidget(m_branch_watch, 0, 2);
|
||||
layout->addWidget(m_code_splitter, 1, 0, -1, -1);
|
||||
|
||||
QWidget* widget = new QWidget(this);
|
||||
|
@ -181,7 +185,7 @@ void CodeWidget::ConnectWidgets()
|
|||
});
|
||||
connect(m_search_callstack, &QLineEdit::textChanged, this, &CodeWidget::UpdateCallstack);
|
||||
|
||||
connect(m_code_diff, &QPushButton::pressed, this, &CodeWidget::OnDiff);
|
||||
connect(m_branch_watch, &QPushButton::pressed, this, &CodeWidget::OnBranchWatchDialog);
|
||||
|
||||
connect(m_symbols_list, &QListWidget::itemPressed, this, &CodeWidget::OnSelectSymbol);
|
||||
connect(m_callstack_list, &QListWidget::itemPressed, this, &CodeWidget::OnSelectCallstack);
|
||||
|
@ -209,15 +213,11 @@ void CodeWidget::ConnectWidgets()
|
|||
connect(m_code_view, &CodeViewWidget::ShowMemory, this, &CodeWidget::ShowMemory);
|
||||
}
|
||||
|
||||
void CodeWidget::OnDiff()
|
||||
void CodeWidget::OnBranchWatchDialog()
|
||||
{
|
||||
if (!m_diff_dialog)
|
||||
m_diff_dialog = new CodeDiffDialog(this);
|
||||
m_diff_dialog->setWindowFlag(Qt::WindowMinimizeButtonHint);
|
||||
SetQWidgetWindowDecorations(m_diff_dialog);
|
||||
m_diff_dialog->show();
|
||||
m_diff_dialog->raise();
|
||||
m_diff_dialog->activateWindow();
|
||||
m_branch_watch_dialog->open();
|
||||
m_branch_watch_dialog->raise();
|
||||
m_branch_watch_dialog->activateWindow();
|
||||
}
|
||||
|
||||
void CodeWidget::OnSearchAddress()
|
||||
|
@ -359,7 +359,7 @@ void CodeWidget::UpdateCallstack()
|
|||
{
|
||||
const QString name = QString::fromStdString(frame.Name.substr(0, frame.Name.length() - 1));
|
||||
|
||||
if (name.toUpper().indexOf(filter.toUpper()) == -1)
|
||||
if (!name.contains(filter, Qt::CaseInsensitive))
|
||||
continue;
|
||||
|
||||
auto* item = new QListWidgetItem(name);
|
||||
|
@ -389,11 +389,15 @@ void CodeWidget::UpdateSymbols()
|
|||
|
||||
item->setData(Qt::UserRole, symbol.second.address);
|
||||
|
||||
if (name.toUpper().indexOf(m_symbol_filter.toUpper()) != -1)
|
||||
if (name.contains(m_symbol_filter, Qt::CaseInsensitive))
|
||||
m_symbols_list->addItem(item);
|
||||
}
|
||||
|
||||
m_symbols_list->sortItems();
|
||||
|
||||
// TODO: There seems to be a lack of a ubiquitous signal for when symbols change.
|
||||
// This is the best location to catch the signals from MenuBar and CodeViewWidget.
|
||||
m_branch_watch_dialog->UpdateSymbols();
|
||||
}
|
||||
|
||||
void CodeWidget::UpdateFunctionCalls(const Common::Symbol* symbol)
|
||||
|
@ -411,7 +415,7 @@ void CodeWidget::UpdateFunctionCalls(const Common::Symbol* symbol)
|
|||
const QString name =
|
||||
QString::fromStdString(fmt::format("> {} ({:08x})", call_symbol->name, addr));
|
||||
|
||||
if (name.toUpper().indexOf(filter.toUpper()) == -1)
|
||||
if (!name.contains(filter, Qt::CaseInsensitive))
|
||||
continue;
|
||||
|
||||
auto* item = new QListWidgetItem(name);
|
||||
|
@ -436,7 +440,7 @@ void CodeWidget::UpdateFunctionCallers(const Common::Symbol* symbol)
|
|||
const QString name =
|
||||
QString::fromStdString(fmt::format("< {} ({:08x})", caller_symbol->name, addr));
|
||||
|
||||
if (name.toUpper().indexOf(filter.toUpper()) == -1)
|
||||
if (!name.contains(filter, Qt::CaseInsensitive))
|
||||
continue;
|
||||
|
||||
auto* item = new QListWidgetItem(name);
|
||||
|
@ -464,6 +468,9 @@ void CodeWidget::Step()
|
|||
power_pc.SetMode(old_mode);
|
||||
Core::DisplayMessage(tr("Step successful!").toStdString(), 2000);
|
||||
// Will get a UpdateDisasmDialog(), don't update the GUI here.
|
||||
|
||||
// TODO: Step doesn't cause EmulationStateChanged to be emitted, so it has to call this manually.
|
||||
m_branch_watch_dialog->Update();
|
||||
}
|
||||
|
||||
void CodeWidget::StepOver()
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
#include <QString>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "DolphinQt/Debugger/CodeDiffDialog.h"
|
||||
#include "DolphinQt/Debugger/CodeViewWidget.h"
|
||||
|
||||
class BranchWatchDialog;
|
||||
class QCloseEvent;
|
||||
class QLineEdit;
|
||||
class QShowEvent;
|
||||
|
@ -41,7 +41,7 @@ public:
|
|||
void ShowPC();
|
||||
void SetPC();
|
||||
|
||||
void OnDiff();
|
||||
void OnBranchWatchDialog();
|
||||
void ToggleBreakpoint();
|
||||
void AddBreakpoint();
|
||||
void SetAddress(u32 address, CodeViewWidget::SetAddressUpdate update);
|
||||
|
@ -72,9 +72,9 @@ private:
|
|||
|
||||
Core::System& m_system;
|
||||
|
||||
CodeDiffDialog* m_diff_dialog = nullptr;
|
||||
BranchWatchDialog* m_branch_watch_dialog;
|
||||
QLineEdit* m_search_address;
|
||||
QPushButton* m_code_diff;
|
||||
QPushButton* m_branch_watch;
|
||||
|
||||
QLineEdit* m_search_callstack;
|
||||
QListWidget* m_callstack_list;
|
||||
|
|
|
@ -137,9 +137,10 @@
|
|||
<ClCompile Include="Debugger\AssembleInstructionDialog.cpp" />
|
||||
<ClCompile Include="Debugger\AssemblerWidget.cpp" />
|
||||
<ClCompile Include="Debugger\AssemblyEditor.cpp" />
|
||||
<ClCompile Include="Debugger\BranchWatchDialog.cpp" />
|
||||
<ClCompile Include="Debugger\BranchWatchTableModel.cpp" />
|
||||
<ClCompile Include="Debugger\BreakpointDialog.cpp" />
|
||||
<ClCompile Include="Debugger\BreakpointWidget.cpp" />
|
||||
<ClCompile Include="Debugger\CodeDiffDialog.cpp" />
|
||||
<ClCompile Include="Debugger\CodeViewWidget.cpp" />
|
||||
<ClCompile Include="Debugger\CodeWidget.cpp" />
|
||||
<ClCompile Include="Debugger\GekkoSyntaxHighlight.cpp" />
|
||||
|
@ -349,9 +350,10 @@
|
|||
<QtMoc Include="Debugger\AssembleInstructionDialog.h" />
|
||||
<QtMoc Include="Debugger\AssemblerWidget.h" />
|
||||
<QtMoc Include="Debugger\AssemblyEditor.h" />
|
||||
<QtMoc Include="Debugger\BranchWatchDialog.h" />
|
||||
<QtMoc Include="Debugger\BranchWatchTableModel.h" />
|
||||
<QtMoc Include="Debugger\BreakpointDialog.h" />
|
||||
<QtMoc Include="Debugger\BreakpointWidget.h" />
|
||||
<QtMoc Include="Debugger\CodeDiffDialog.h" />
|
||||
<QtMoc Include="Debugger\CodeViewWidget.h" />
|
||||
<QtMoc Include="Debugger\CodeWidget.h" />
|
||||
<QtMoc Include="Debugger\GekkoSyntaxHighlight.h" />
|
||||
|
|
|
@ -410,6 +410,12 @@ void HotkeyScheduler::Run()
|
|||
case AspectMode::Custom:
|
||||
OSD::AddMessage("Custom");
|
||||
break;
|
||||
case AspectMode::CustomStretch:
|
||||
OSD::AddMessage("Custom (Stretch)");
|
||||
break;
|
||||
case AspectMode::Raw:
|
||||
OSD::AddMessage("Raw (Square Pixels)");
|
||||
break;
|
||||
case AspectMode::Auto:
|
||||
default:
|
||||
OSD::AddMessage("Auto");
|
||||
|
|
|
@ -74,6 +74,8 @@ static void CreateDumpPath(std::string path)
|
|||
File::CreateFullPath(File::GetUserPath(D_DUMPFRAMES_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPOBJECTS_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPTEXTURES_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX));
|
||||
}
|
||||
|
||||
static void CreateLoadPath(std::string path)
|
||||
|
@ -253,6 +255,8 @@ void CreateDirectories()
|
|||
File::CreateFullPath(File::GetUserPath(D_DUMPDSP_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPSSL_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPTEXTURES_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_GAMESETTINGS_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX));
|
||||
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + USA_DIR DIR_SEP);
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
#include "VideoCommon/Assets/CustomAssetLoader.h"
|
||||
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/MemoryUtil.h"
|
||||
#include "VideoCommon/Assets/CustomAssetLibrary.h"
|
||||
|
||||
|
@ -48,19 +47,22 @@ void CustomAssetLoader::Init()
|
|||
m_asset_load_thread.Reset("Custom Asset Loader", [this](std::weak_ptr<CustomAsset> asset) {
|
||||
if (auto ptr = asset.lock())
|
||||
{
|
||||
if (m_memory_exceeded)
|
||||
return;
|
||||
|
||||
if (ptr->Load())
|
||||
{
|
||||
std::lock_guard lk(m_asset_load_lock);
|
||||
const std::size_t asset_memory_size = ptr->GetByteSizeInMemory();
|
||||
if (m_max_memory_available >= m_total_bytes_loaded + asset_memory_size)
|
||||
m_total_bytes_loaded += asset_memory_size;
|
||||
m_assets_to_monitor.try_emplace(ptr->GetAssetId(), ptr);
|
||||
if (m_total_bytes_loaded > m_max_memory_available)
|
||||
{
|
||||
m_total_bytes_loaded += asset_memory_size;
|
||||
m_assets_to_monitor.try_emplace(ptr->GetAssetId(), ptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
ERROR_LOG_FMT(VIDEO, "Failed to load asset {} because there was not enough memory.",
|
||||
ERROR_LOG_FMT(VIDEO,
|
||||
"Asset memory exceeded with asset '{}', future assets won't load until "
|
||||
"memory is available.",
|
||||
ptr->GetAssetId());
|
||||
m_memory_exceeded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <thread>
|
||||
|
||||
#include "Common/Flag.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Common/WorkQueueThread.h"
|
||||
#include "VideoCommon/Assets/CustomAsset.h"
|
||||
#include "VideoCommon/Assets/MaterialAsset.h"
|
||||
|
@ -71,6 +72,11 @@ private:
|
|||
std::lock_guard lk(m_asset_load_lock);
|
||||
m_total_bytes_loaded -= a->GetByteSizeInMemory();
|
||||
m_assets_to_monitor.erase(a->GetAssetId());
|
||||
if (m_max_memory_available >= m_total_bytes_loaded && m_memory_exceeded)
|
||||
{
|
||||
INFO_LOG_FMT(VIDEO, "Asset memory went below limit, new assets can begin loading.");
|
||||
m_memory_exceeded = false;
|
||||
}
|
||||
}
|
||||
delete a;
|
||||
});
|
||||
|
@ -90,6 +96,7 @@ private:
|
|||
|
||||
std::size_t m_total_bytes_loaded = 0;
|
||||
std::size_t m_max_memory_available = 0;
|
||||
std::atomic_bool m_memory_exceeded = false;
|
||||
|
||||
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<CustomAsset>> m_assets_to_monitor;
|
||||
|
||||
|
|
|
@ -194,7 +194,7 @@ protected:
|
|||
void DoLoadState(PointerWrap& p);
|
||||
void DoSaveState(PointerWrap& p);
|
||||
|
||||
float m_efb_scale = 0.0f;
|
||||
float m_efb_scale = 1.0f;
|
||||
PixelFormat m_prev_efb_format;
|
||||
|
||||
std::unique_ptr<AbstractTexture> m_efb_color_texture;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <picojson.h>
|
||||
|
@ -21,6 +22,7 @@ public:
|
|||
std::string m_pixel_material_asset;
|
||||
};
|
||||
|
||||
static constexpr std::string_view factory_name = "custom_pipeline";
|
||||
static std::unique_ptr<CustomPipelineAction>
|
||||
Create(const picojson::value& json_data,
|
||||
std::shared_ptr<VideoCommon::CustomAssetLibrary> library);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include <picojson.h>
|
||||
|
||||
|
@ -12,6 +13,7 @@
|
|||
class MoveAction final : public GraphicsModAction
|
||||
{
|
||||
public:
|
||||
static constexpr std::string_view factory_name = "move";
|
||||
static std::unique_ptr<MoveAction> Create(const picojson::value& json_data);
|
||||
explicit MoveAction(Common::Vec3 position_offset);
|
||||
void OnProjection(GraphicsModActionData::Projection* projection) override;
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "VideoCommon/GraphicsModSystem/Runtime/GraphicsModAction.h"
|
||||
|
||||
class PrintAction final : public GraphicsModAction
|
||||
{
|
||||
public:
|
||||
static constexpr std::string_view factory_name = "print";
|
||||
void OnDrawStarted(GraphicsModActionData::DrawStarted*) override;
|
||||
void OnEFB(GraphicsModActionData::EFB*) override;
|
||||
void OnProjection(GraphicsModActionData::Projection*) override;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include <picojson.h>
|
||||
|
||||
|
@ -12,6 +13,7 @@
|
|||
class ScaleAction final : public GraphicsModAction
|
||||
{
|
||||
public:
|
||||
static constexpr std::string_view factory_name = "scale";
|
||||
static std::unique_ptr<ScaleAction> Create(const picojson::value& json_data);
|
||||
explicit ScaleAction(Common::Vec3 scale);
|
||||
void OnEFB(GraphicsModActionData::EFB*) override;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
class SkipAction final : public GraphicsModAction
|
||||
{
|
||||
public:
|
||||
static constexpr std::string_view factory_name = "skip";
|
||||
void OnDrawStarted(GraphicsModActionData::DrawStarted*) override;
|
||||
void OnEFB(GraphicsModActionData::EFB*) override;
|
||||
};
|
||||
|
|
|
@ -14,23 +14,23 @@ namespace GraphicsModActionFactory
|
|||
std::unique_ptr<GraphicsModAction> Create(std::string_view name, const picojson::value& json_data,
|
||||
std::shared_ptr<VideoCommon::CustomAssetLibrary> library)
|
||||
{
|
||||
if (name == "print")
|
||||
if (name == PrintAction::factory_name)
|
||||
{
|
||||
return std::make_unique<PrintAction>();
|
||||
}
|
||||
else if (name == "skip")
|
||||
else if (name == SkipAction::factory_name)
|
||||
{
|
||||
return std::make_unique<SkipAction>();
|
||||
}
|
||||
else if (name == "move")
|
||||
else if (name == MoveAction::factory_name)
|
||||
{
|
||||
return MoveAction::Create(json_data);
|
||||
}
|
||||
else if (name == "scale")
|
||||
else if (name == ScaleAction::factory_name)
|
||||
{
|
||||
return ScaleAction::Create(json_data);
|
||||
}
|
||||
else if (name == "custom_pipeline")
|
||||
else if (name == CustomPipelineAction::factory_name)
|
||||
{
|
||||
return CustomPipelineAction::Create(json_data, std::move(library));
|
||||
}
|
||||
|
|
|
@ -1000,10 +1000,15 @@ ShaderCode GeneratePixelShaderCode(APIType api_type, const ShaderHostConfig& hos
|
|||
else
|
||||
#endif
|
||||
{
|
||||
out.Write("{} {} {} {};\n", "FRAGMENT_OUTPUT_LOCATION_INDEXED(0, 0)",
|
||||
use_framebuffer_fetch ? "FRAGMENT_INOUT" : "out",
|
||||
uid_data->uint_output ? "uvec4" : "vec4",
|
||||
use_framebuffer_fetch ? "real_ocol0" : "ocol0");
|
||||
if (use_framebuffer_fetch)
|
||||
{
|
||||
out.Write("FRAGMENT_OUTPUT_LOCATION(0) FRAGMENT_INOUT vec4 real_ocol0;\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
out.Write("FRAGMENT_OUTPUT_LOCATION_INDEXED(0, 0) out {} ocol0;\n",
|
||||
uid_data->uint_output ? "uvec4" : "vec4");
|
||||
}
|
||||
|
||||
if (!uid_data->no_dual_src)
|
||||
{
|
||||
|
|
|
@ -69,6 +69,32 @@ static std::tuple<int, int> FindClosestIntegerResolution(float width, float heig
|
|||
return std::make_tuple(int_width, int_height);
|
||||
}
|
||||
|
||||
static void TryToSnapToXFBSize(int& width, int& height, int xfb_width, int xfb_height)
|
||||
{
|
||||
// Screen is blanking (e.g. game booting up), nothing to do here
|
||||
if (xfb_width == 0 || xfb_height == 0)
|
||||
return;
|
||||
|
||||
// If there's only 1 pixel of either horizontal or vertical resolution difference,
|
||||
// make the output size match a multiple of the XFB native resolution,
|
||||
// to achieve the highest quality (least scaling).
|
||||
// The reason why the threshold is 1 pixel (per internal resolution multiplier) is because of
|
||||
// minor inaccuracies of the VI aspect ratio (and because some resolutions are rounded
|
||||
// while other are floored).
|
||||
const unsigned int efb_scale = g_framebuffer_manager->GetEFBScale();
|
||||
const unsigned int pixel_difference_width = std::abs(width - xfb_width);
|
||||
const unsigned int pixel_difference_height = std::abs(height - xfb_height);
|
||||
// We ignore this if there's an offset on both hor and ver size,
|
||||
// as then we'd be changing the aspect ratio too much and would need to
|
||||
// re-calculate a lot of stuff (like black bars).
|
||||
if ((pixel_difference_width <= efb_scale && pixel_difference_height == 0) ||
|
||||
(pixel_difference_height <= efb_scale && pixel_difference_width == 0))
|
||||
{
|
||||
width = xfb_width;
|
||||
height = xfb_height;
|
||||
}
|
||||
}
|
||||
|
||||
Presenter::Presenter()
|
||||
{
|
||||
m_config_changed =
|
||||
|
@ -114,6 +140,7 @@ bool Presenter::FetchXFB(u32 xfb_addr, u32 fb_width, u32 fb_stride, u32 fb_heigh
|
|||
{
|
||||
// Game is blanking the screen
|
||||
m_xfb_entry.reset();
|
||||
m_xfb_rect = MathUtil::Rectangle<int>();
|
||||
m_last_xfb_id = std::numeric_limits<u64>::max();
|
||||
}
|
||||
else
|
||||
|
@ -329,12 +356,20 @@ float Presenter::CalculateDrawAspectRatio(bool allow_stretch) const
|
|||
{
|
||||
return SourceAspectRatioToWidescreen(source_aspect_ratio);
|
||||
}
|
||||
// For the "custom" mode, we force the exact target aspect ratio, without
|
||||
// acknowleding the difference between the source aspect ratio and 4:3.
|
||||
else if (aspect_mode == AspectMode::Custom)
|
||||
{
|
||||
return source_aspect_ratio * (g_ActiveConfig.GetCustomAspectRatio() / (4.0f / 3.0f));
|
||||
}
|
||||
// For the "custom stretch" mode, we force the exact target aspect ratio, without
|
||||
// acknowleding the difference between the source aspect ratio and 4:3.
|
||||
else if (aspect_mode == AspectMode::CustomStretch)
|
||||
{
|
||||
return g_ActiveConfig.GetCustomAspectRatio();
|
||||
}
|
||||
else if (aspect_mode == AspectMode::Raw)
|
||||
{
|
||||
return m_xfb_entry ? (static_cast<float>(m_last_xfb_width) / m_last_xfb_height) : 1.f;
|
||||
}
|
||||
|
||||
return source_aspect_ratio;
|
||||
}
|
||||
|
@ -401,9 +436,11 @@ void* Presenter::GetNewSurfaceHandle()
|
|||
|
||||
u32 Presenter::AutoIntegralScale() const
|
||||
{
|
||||
// Take the source resolution (XFB) and stretch it on the target aspect ratio.
|
||||
// Take the source/native resolution (XFB) and stretch it on the target (window) aspect ratio.
|
||||
// If the target resolution is larger (on either x or y), we scale the source
|
||||
// by a integer multiplier until it won't have to be scaled up anymore.
|
||||
// NOTE: this might conflict with "Config::MAIN_RENDER_WINDOW_AUTOSIZE",
|
||||
// as they mutually influence each other.
|
||||
u32 source_width = m_last_xfb_width;
|
||||
u32 source_height = m_last_xfb_height;
|
||||
const u32 target_width = m_target_rectangle.GetWidth();
|
||||
|
@ -450,7 +487,7 @@ std::tuple<float, float> Presenter::ApplyStandardAspectCrop(float width, float h
|
|||
if (!allow_stretch && aspect_mode == AspectMode::Stretch)
|
||||
aspect_mode = AspectMode::Auto;
|
||||
|
||||
if (!g_ActiveConfig.bCrop || aspect_mode == AspectMode::Stretch)
|
||||
if (!g_ActiveConfig.bCrop || aspect_mode == AspectMode::Stretch || aspect_mode == AspectMode::Raw)
|
||||
return {width, height};
|
||||
|
||||
// Force aspect ratios by cropping the image.
|
||||
|
@ -468,9 +505,12 @@ std::tuple<float, float> Presenter::ApplyStandardAspectCrop(float width, float h
|
|||
case AspectMode::ForceStandard:
|
||||
expected_aspect = 4.0f / 3.0f;
|
||||
break;
|
||||
// There should be no cropping needed in the custom case,
|
||||
// as output should always exactly match the target aspect ratio
|
||||
// For the custom (relative) case, we want to crop from the native aspect ratio
|
||||
// to the specific target one, as they likely have a small difference
|
||||
case AspectMode::Custom:
|
||||
// There should be no cropping needed in the custom strech case,
|
||||
// as output should always exactly match the target aspect ratio
|
||||
case AspectMode::CustomStretch:
|
||||
expected_aspect = g_ActiveConfig.GetCustomAspectRatio();
|
||||
break;
|
||||
}
|
||||
|
@ -534,6 +574,7 @@ void Presenter::UpdateDrawRectangle()
|
|||
// FIXME: this breaks at very low widget sizes
|
||||
// Make ControllerInterface aware of the render window region actually being used
|
||||
// to adjust mouse cursor inputs.
|
||||
// This also fails to acknowledge "g_ActiveConfig.bCrop".
|
||||
g_controller_interface.SetAspectRatioAdjustment(draw_aspect_ratio / win_aspect_ratio);
|
||||
|
||||
float draw_width = draw_aspect_ratio;
|
||||
|
@ -574,12 +615,31 @@ void Presenter::UpdateDrawRectangle()
|
|||
int_draw_width = static_cast<int>(draw_width);
|
||||
int_draw_height = static_cast<int>(draw_height);
|
||||
}
|
||||
else
|
||||
else if (g_ActiveConfig.aspect_mode != AspectMode::Raw || !m_xfb_entry)
|
||||
{
|
||||
// Find the best integer resolution: the closest aspect ratio with the least black bars.
|
||||
// This should have no influence if "AspectMode::Stretch" is active.
|
||||
const float updated_draw_aspect_ratio = draw_width / draw_height;
|
||||
const auto int_draw_res =
|
||||
FindClosestIntegerResolution(draw_width, draw_height, win_aspect_ratio);
|
||||
FindClosestIntegerResolution(draw_width, draw_height, updated_draw_aspect_ratio);
|
||||
int_draw_width = std::get<0>(int_draw_res);
|
||||
int_draw_height = std::get<1>(int_draw_res);
|
||||
if (!g_ActiveConfig.bCrop)
|
||||
{
|
||||
if (g_ActiveConfig.aspect_mode != AspectMode::Stretch)
|
||||
{
|
||||
TryToSnapToXFBSize(int_draw_width, int_draw_height, m_xfb_rect.GetWidth(),
|
||||
m_xfb_rect.GetHeight());
|
||||
}
|
||||
// We can't draw something bigger than the window, it will crop
|
||||
int_draw_width = std::min(int_draw_width, static_cast<int>(win_width));
|
||||
int_draw_height = std::min(int_draw_height, static_cast<int>(win_height));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int_draw_width = m_xfb_rect.GetWidth();
|
||||
int_draw_height = m_xfb_rect.GetHeight();
|
||||
}
|
||||
|
||||
m_target_rectangle.left = static_cast<int>(std::round(win_width / 2.0 - int_draw_width / 2.0));
|
||||
|
@ -620,13 +680,17 @@ std::tuple<int, int> Presenter::CalculateOutputDimensions(int width, int height,
|
|||
if (!allow_stretch && aspect_mode == AspectMode::Stretch)
|
||||
aspect_mode = AspectMode::Auto;
|
||||
|
||||
// Find the closest integer aspect ratio,
|
||||
// this avoids a small black line from being drawn on one of the four edges
|
||||
if (!g_ActiveConfig.bCrop && aspect_mode != AspectMode::Stretch)
|
||||
{
|
||||
// Find the closest integer resolution for the aspect ratio,
|
||||
// this avoids a small black line from being drawn on one of the four edges
|
||||
const float draw_aspect_ratio = CalculateDrawAspectRatio(allow_stretch);
|
||||
const auto [int_width, int_height] =
|
||||
auto [int_width, int_height] =
|
||||
FindClosestIntegerResolution(scaled_width, scaled_height, draw_aspect_ratio);
|
||||
if (aspect_mode != AspectMode::Raw)
|
||||
{
|
||||
TryToSnapToXFBSize(int_width, int_height, m_xfb_rect.GetWidth(), m_xfb_rect.GetHeight());
|
||||
}
|
||||
width = int_width;
|
||||
height = int_height;
|
||||
}
|
||||
|
|
|
@ -138,7 +138,8 @@ private:
|
|||
u32 m_auto_resolution_scale = 1;
|
||||
|
||||
RcTcacheEntry m_xfb_entry;
|
||||
MathUtil::Rectangle<int> m_xfb_rect;
|
||||
// Internal resolution multiplier scaled XFB size
|
||||
MathUtil::Rectangle<int> m_xfb_rect{0, 0, MAX_XFB_WIDTH, MAX_XFB_HEIGHT};
|
||||
|
||||
// Tracking of XFB textures so we don't render duplicate frames.
|
||||
u64 m_last_xfb_id = std::numeric_limits<u64>::max();
|
||||
|
@ -156,8 +157,10 @@ private:
|
|||
// XFB tracking
|
||||
u64 m_last_xfb_ticks = 0;
|
||||
u32 m_last_xfb_addr = 0;
|
||||
// Native XFB width
|
||||
u32 m_last_xfb_width = MAX_XFB_WIDTH;
|
||||
u32 m_last_xfb_stride = 0;
|
||||
// Native XFB height
|
||||
u32 m_last_xfb_height = MAX_XFB_HEIGHT;
|
||||
|
||||
Common::EventHook m_config_changed;
|
||||
|
|
|
@ -370,10 +370,15 @@ ShaderCode GenPixelShader(APIType api_type, const ShaderHostConfig& host_config,
|
|||
else
|
||||
#endif
|
||||
{
|
||||
out.Write("{} {} {} {};\n", "FRAGMENT_OUTPUT_LOCATION_INDEXED(0, 0)",
|
||||
use_framebuffer_fetch ? "FRAGMENT_INOUT" : "out",
|
||||
uid_data->uint_output ? "uvec4" : "vec4",
|
||||
use_framebuffer_fetch ? "real_ocol0" : "ocol0");
|
||||
if (use_framebuffer_fetch)
|
||||
{
|
||||
out.Write("FRAGMENT_OUTPUT_LOCATION(0) FRAGMENT_INOUT vec4 real_ocol0;\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
out.Write("FRAGMENT_OUTPUT_LOCATION_INDEXED(0, 0) out {} ocol0;\n",
|
||||
uid_data->uint_output ? "uvec4" : "vec4");
|
||||
}
|
||||
|
||||
if (use_dual_source)
|
||||
{
|
||||
|
|
|
@ -247,7 +247,7 @@ private:
|
|||
bool m_allow_background_execution = true;
|
||||
|
||||
std::unique_ptr<CustomShaderCache> m_custom_shader_cache;
|
||||
u64 m_ticks_elapsed;
|
||||
u64 m_ticks_elapsed = 0;
|
||||
|
||||
Common::EventHook m_frame_end_event;
|
||||
Common::EventHook m_after_present_event;
|
||||
|
|
|
@ -21,11 +21,13 @@ constexpr int EFB_SCALE_AUTO_INTEGRAL = 0;
|
|||
|
||||
enum class AspectMode : int
|
||||
{
|
||||
Auto, // 4:3 or 16:9
|
||||
ForceWide, // 16:9
|
||||
ForceStandard, // 4:3
|
||||
Auto, // ~4:3 or ~16:9 (auto detected)
|
||||
ForceWide, // ~16:9
|
||||
ForceStandard, // ~4:3
|
||||
Stretch,
|
||||
Custom,
|
||||
Custom, // Forced relative custom AR
|
||||
CustomStretch, // Forced absolute custom AR
|
||||
Raw, // Forced squared pixels
|
||||
};
|
||||
|
||||
enum class StereoMode : int
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "VideoCommon/Widescreen.h"
|
||||
|
||||
#include "Common/ChunkFile.h"
|
||||
#include "Common/Logging/Log.h"
|
||||
#include "Core/Config/SYSCONFSettings.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
|
@ -13,12 +14,34 @@ std::unique_ptr<WidescreenManager> g_widescreen;
|
|||
|
||||
WidescreenManager::WidescreenManager()
|
||||
{
|
||||
Update();
|
||||
std::optional<bool> is_game_widescreen = GetWidescreenOverride();
|
||||
if (is_game_widescreen.has_value())
|
||||
m_is_game_widescreen = is_game_widescreen.value();
|
||||
|
||||
// Throw a warning as unsupported aspect ratio modes have no specific behavior to them
|
||||
const bool is_valid_suggested_aspect_mode =
|
||||
g_ActiveConfig.suggested_aspect_mode == AspectMode::Auto ||
|
||||
g_ActiveConfig.suggested_aspect_mode == AspectMode::ForceStandard ||
|
||||
g_ActiveConfig.suggested_aspect_mode == AspectMode::ForceWide;
|
||||
if (!is_valid_suggested_aspect_mode)
|
||||
{
|
||||
WARN_LOG_FMT(VIDEO,
|
||||
"Invalid suggested aspect ratio mode: only Auto, 4:3 and 16:9 are supported");
|
||||
}
|
||||
|
||||
m_config_changed = ConfigChangedEvent::Register(
|
||||
[this](u32 bits) {
|
||||
if (bits & (CONFIG_CHANGE_BIT_ASPECT_RATIO))
|
||||
Update();
|
||||
{
|
||||
std::optional<bool> is_game_widescreen = GetWidescreenOverride();
|
||||
// If the widescreen flag isn't being overridden by any settings,
|
||||
// reset it to default if heuristic aren't running or to the last
|
||||
// heuristic value if they were running.
|
||||
if (!is_game_widescreen.has_value())
|
||||
is_game_widescreen = (m_heuristic_state == HeuristicState::Active_Found_Anamorphic);
|
||||
if (is_game_widescreen.has_value())
|
||||
m_is_game_widescreen = is_game_widescreen.value();
|
||||
}
|
||||
},
|
||||
"Widescreen");
|
||||
|
||||
|
@ -31,80 +54,100 @@ WidescreenManager::WidescreenManager()
|
|||
}
|
||||
}
|
||||
|
||||
void WidescreenManager::Update()
|
||||
std::optional<bool> WidescreenManager::GetWidescreenOverride() const
|
||||
{
|
||||
std::optional<bool> is_game_widescreen;
|
||||
|
||||
auto& system = Core::System::GetInstance();
|
||||
if (system.IsWii())
|
||||
m_is_game_widescreen = Config::Get(Config::SYSCONF_WIDESCREEN);
|
||||
is_game_widescreen = Config::Get(Config::SYSCONF_WIDESCREEN);
|
||||
|
||||
// suggested_aspect_mode overrides SYSCONF_WIDESCREEN
|
||||
if (g_ActiveConfig.suggested_aspect_mode == AspectMode::ForceStandard)
|
||||
m_is_game_widescreen = false;
|
||||
is_game_widescreen = false;
|
||||
else if (g_ActiveConfig.suggested_aspect_mode == AspectMode::ForceWide)
|
||||
m_is_game_widescreen = true;
|
||||
is_game_widescreen = true;
|
||||
|
||||
// If widescreen hack is disabled override game's AR if UI is set to 4:3 or 16:9.
|
||||
if (!g_ActiveConfig.bWidescreenHack)
|
||||
{
|
||||
const auto aspect_mode = g_ActiveConfig.aspect_mode;
|
||||
if (aspect_mode == AspectMode::ForceStandard)
|
||||
m_is_game_widescreen = false;
|
||||
is_game_widescreen = false;
|
||||
else if (aspect_mode == AspectMode::ForceWide)
|
||||
m_is_game_widescreen = true;
|
||||
is_game_widescreen = true;
|
||||
}
|
||||
|
||||
return is_game_widescreen;
|
||||
}
|
||||
|
||||
// Heuristic to detect if a GameCube game is in 16:9 anamorphic widescreen mode.
|
||||
// Cheats that change the game aspect ratio to natively unsupported ones won't be recognized here.
|
||||
void WidescreenManager::UpdateWidescreenHeuristic()
|
||||
{
|
||||
// Reset to baseline state before the update
|
||||
const auto flush_statistics = g_vertex_manager->ResetFlushAspectRatioCount();
|
||||
const bool was_orthographically_anamorphic = m_was_orthographically_anamorphic;
|
||||
m_heuristic_state = HeuristicState::Inactive;
|
||||
m_was_orthographically_anamorphic = false;
|
||||
|
||||
// If suggested_aspect_mode (GameINI) is configured don't use heuristic.
|
||||
// We also don't need to check "GetWidescreenOverride()" in this case as
|
||||
// nothing would have changed there.
|
||||
if (g_ActiveConfig.suggested_aspect_mode != AspectMode::Auto)
|
||||
return;
|
||||
|
||||
Update();
|
||||
std::optional<bool> is_game_widescreen = GetWidescreenOverride();
|
||||
|
||||
// If widescreen hack isn't active and aspect_mode (user setting)
|
||||
// is set to a forced aspect ratio, don't use heuristic.
|
||||
if (!g_ActiveConfig.bWidescreenHack && (g_ActiveConfig.aspect_mode == AspectMode::ForceStandard ||
|
||||
g_ActiveConfig.aspect_mode == AspectMode::ForceWide))
|
||||
return;
|
||||
|
||||
// Modify the threshold based on which aspect ratio we're already using:
|
||||
// If the game's in 4:3, it probably won't switch to anamorphic, and vice-versa.
|
||||
const u32 transition_threshold = g_ActiveConfig.widescreen_heuristic_transition_threshold;
|
||||
|
||||
const auto looks_normal = [transition_threshold](auto& counts) {
|
||||
return counts.normal_vertex_count > counts.anamorphic_vertex_count * transition_threshold;
|
||||
};
|
||||
const auto looks_anamorphic = [transition_threshold](auto& counts) {
|
||||
return counts.anamorphic_vertex_count > counts.normal_vertex_count * transition_threshold;
|
||||
};
|
||||
|
||||
const auto& persp = flush_statistics.perspective;
|
||||
const auto& ortho = flush_statistics.orthographic;
|
||||
|
||||
const auto ortho_looks_anamorphic = looks_anamorphic(ortho);
|
||||
|
||||
if (looks_anamorphic(persp) || ortho_looks_anamorphic)
|
||||
// If widescreen hack isn't active and aspect_mode (UI) is 4:3 or 16:9 don't use heuristic.
|
||||
if (g_ActiveConfig.bWidescreenHack || (g_ActiveConfig.aspect_mode != AspectMode::ForceStandard &&
|
||||
g_ActiveConfig.aspect_mode != AspectMode::ForceWide))
|
||||
{
|
||||
// If either perspective or orthographic projections look anamorphic, it's a safe bet.
|
||||
m_is_game_widescreen = true;
|
||||
}
|
||||
else if (looks_normal(persp) || (m_was_orthographically_anamorphic && looks_normal(ortho)))
|
||||
{
|
||||
// Many widescreen games (or AR/GeckoCodes) use anamorphic perspective projections
|
||||
// with NON-anamorphic orthographic projections.
|
||||
// This can cause incorrect changes to 4:3 when perspective projections are temporarily not
|
||||
// shown. e.g. Animal Crossing's inventory menu.
|
||||
// Unless we were in a situation which was orthographically anamorphic
|
||||
// we won't consider orthographic data for changes from 16:9 to 4:3.
|
||||
m_is_game_widescreen = false;
|
||||
// Modify the threshold based on which aspect ratio we're already using:
|
||||
// If the game's in 4:3, it probably won't switch to anamorphic, and vice-versa.
|
||||
const u32 transition_threshold = g_ActiveConfig.widescreen_heuristic_transition_threshold;
|
||||
|
||||
const auto looks_normal = [transition_threshold](auto& counts) {
|
||||
return counts.normal_vertex_count > counts.anamorphic_vertex_count * transition_threshold;
|
||||
};
|
||||
const auto looks_anamorphic = [transition_threshold](auto& counts) {
|
||||
return counts.anamorphic_vertex_count > counts.normal_vertex_count * transition_threshold;
|
||||
};
|
||||
|
||||
const auto& persp = flush_statistics.perspective;
|
||||
const auto& ortho = flush_statistics.orthographic;
|
||||
|
||||
const auto ortho_looks_anamorphic = looks_anamorphic(ortho);
|
||||
const auto persp_looks_normal = looks_normal(persp);
|
||||
|
||||
if (looks_anamorphic(persp) || ortho_looks_anamorphic)
|
||||
{
|
||||
// If either perspective or orthographic projections look anamorphic, it's a safe bet.
|
||||
is_game_widescreen = true;
|
||||
m_heuristic_state = HeuristicState::Active_Found_Anamorphic;
|
||||
}
|
||||
else if (persp_looks_normal || looks_normal(ortho))
|
||||
{
|
||||
// Many widescreen games (or AR/GeckoCodes) use anamorphic perspective projections
|
||||
// with NON-anamorphic orthographic projections.
|
||||
// This can cause incorrect changes to 4:3 when perspective projections are temporarily not
|
||||
// shown. e.g. Animal Crossing's inventory menu.
|
||||
// Unless we were in a situation which was orthographically anamorphic
|
||||
// we won't consider orthographic data for changes from 16:9 to 4:3.
|
||||
if (persp_looks_normal || was_orthographically_anamorphic)
|
||||
is_game_widescreen = false;
|
||||
m_heuristic_state = HeuristicState::Active_Found_Normal;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_heuristic_state = HeuristicState::Active_NotFound;
|
||||
}
|
||||
|
||||
m_was_orthographically_anamorphic = ortho_looks_anamorphic;
|
||||
}
|
||||
|
||||
m_was_orthographically_anamorphic = ortho_looks_anamorphic;
|
||||
if (is_game_widescreen.has_value())
|
||||
m_is_game_widescreen = is_game_widescreen.value();
|
||||
}
|
||||
|
||||
void WidescreenManager::DoState(PointerWrap& p)
|
||||
|
@ -114,5 +157,6 @@ void WidescreenManager::DoState(PointerWrap& p)
|
|||
if (p.IsReadMode())
|
||||
{
|
||||
m_was_orthographically_anamorphic = false;
|
||||
m_heuristic_state = HeuristicState::Inactive;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,11 +24,21 @@ public:
|
|||
void DoState(PointerWrap& p);
|
||||
|
||||
private:
|
||||
void Update();
|
||||
enum class HeuristicState
|
||||
{
|
||||
Inactive,
|
||||
Active_NotFound,
|
||||
Active_Found_Normal,
|
||||
Active_Found_Anamorphic,
|
||||
};
|
||||
|
||||
// Returns whether the widescreen state wants to change, and its target value
|
||||
std::optional<bool> GetWidescreenOverride() const;
|
||||
void UpdateWidescreenHeuristic();
|
||||
|
||||
bool m_is_game_widescreen = false;
|
||||
bool m_was_orthographically_anamorphic = false;
|
||||
HeuristicState m_heuristic_state = HeuristicState::Inactive;
|
||||
|
||||
Common::EventHook m_update_widescreen;
|
||||
Common::EventHook m_config_changed;
|
||||
|
|
|
@ -15,6 +15,7 @@ add_dolphin_test(FlagTest FlagTest.cpp)
|
|||
add_dolphin_test(FloatUtilsTest FloatUtilsTest.cpp)
|
||||
add_dolphin_test(MathUtilTest MathUtilTest.cpp)
|
||||
add_dolphin_test(NandPathsTest NandPathsTest.cpp)
|
||||
add_dolphin_test(SettingsHandlerTest SettingsHandlerTest.cpp)
|
||||
add_dolphin_test(SPSCQueueTest SPSCQueueTest.cpp)
|
||||
add_dolphin_test(StringUtilTest StringUtilTest.cpp)
|
||||
add_dolphin_test(SwapTest SwapTest.cpp)
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright 2024 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Common/SettingsHandler.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// The encrypted bytes corresponding to the following settings, in order:
|
||||
// "key" = "val"
|
||||
Common::SettingsHandler::Buffer BUFFER_A{0x91, 0x91, 0x90, 0xEE, 0xD1, 0x2F, 0xF0, 0x34, 0x79};
|
||||
|
||||
// The encrypted bytes corresponding to the following settings, in order:
|
||||
// "key1" = "val1"
|
||||
// "key2" = "val2"
|
||||
// "foo" = "bar"
|
||||
Common::SettingsHandler::Buffer BUFFER_B{
|
||||
0x91, 0x91, 0x90, 0xE2, 0x9A, 0x38, 0xFD, 0x55, 0x42, 0xEA, 0xC4, 0xF6, 0x5E, 0xF, 0xDF, 0xE7,
|
||||
0xC3, 0x0A, 0xBB, 0x9C, 0x50, 0xB1, 0x10, 0x82, 0xB4, 0x8A, 0x0D, 0xBE, 0xCD, 0x72, 0xF4};
|
||||
} // namespace
|
||||
|
||||
TEST(SettingsHandlerTest, EncryptSingleSetting)
|
||||
{
|
||||
Common::SettingsHandler handler;
|
||||
handler.AddSetting("key", "val");
|
||||
Common::SettingsHandler::Buffer buffer = handler.GetBytes();
|
||||
|
||||
EXPECT_TRUE(std::equal(buffer.begin(), buffer.end(), BUFFER_A.begin(), BUFFER_A.end()));
|
||||
}
|
||||
|
||||
TEST(SettingsHandlerTest, DecryptSingleSetting)
|
||||
{
|
||||
Common::SettingsHandler::Buffer buffer = BUFFER_A;
|
||||
Common::SettingsHandler handler(std::move(buffer));
|
||||
EXPECT_EQ(handler.GetValue("key"), "val");
|
||||
}
|
||||
|
||||
TEST(SettingsHandlerTest, EncryptMultipleSettings)
|
||||
{
|
||||
Common::SettingsHandler handler;
|
||||
handler.AddSetting("key1", "val1");
|
||||
handler.AddSetting("key2", "val2");
|
||||
handler.AddSetting("foo", "bar");
|
||||
Common::SettingsHandler::Buffer buffer = handler.GetBytes();
|
||||
|
||||
EXPECT_TRUE(std::equal(buffer.begin(), buffer.end(), BUFFER_B.begin(), BUFFER_B.end()));
|
||||
}
|
||||
|
||||
TEST(SettingsHandlerTest, DecryptMultipleSettings)
|
||||
{
|
||||
Common::SettingsHandler::Buffer buffer = BUFFER_B;
|
||||
Common::SettingsHandler handler(std::move(buffer));
|
||||
EXPECT_EQ(handler.GetValue("key1"), "val1");
|
||||
EXPECT_EQ(handler.GetValue("key2"), "val2");
|
||||
EXPECT_EQ(handler.GetValue("foo"), "bar");
|
||||
}
|
||||
|
||||
TEST(SettingsHandlerTest, SetBytesOverwritesExistingBuffer)
|
||||
{
|
||||
Common::SettingsHandler::Buffer buffer = BUFFER_A;
|
||||
Common::SettingsHandler handler(std::move(buffer));
|
||||
ASSERT_EQ(handler.GetValue("key"), "val");
|
||||
ASSERT_EQ(handler.GetValue("foo"), "");
|
||||
|
||||
Common::SettingsHandler::Buffer buffer2 = BUFFER_B;
|
||||
handler.SetBytes(std::move(buffer2));
|
||||
EXPECT_EQ(handler.GetValue("foo"), "bar");
|
||||
EXPECT_EQ(handler.GetValue("key"), "");
|
||||
}
|
||||
|
||||
TEST(SettingsHandlerTest, GetValueOnSameInstance)
|
||||
{
|
||||
Common::SettingsHandler handler;
|
||||
handler.AddSetting("key", "val");
|
||||
EXPECT_EQ(handler.GetValue("key"), "");
|
||||
|
||||
Common::SettingsHandler::Buffer buffer = handler.GetBytes();
|
||||
handler.SetBytes(std::move(buffer));
|
||||
EXPECT_EQ(handler.GetValue("key"), "val");
|
||||
}
|
||||
|
||||
TEST(SettingsHandlerTest, GetValueAfterReset)
|
||||
{
|
||||
Common::SettingsHandler::Buffer buffer = BUFFER_A;
|
||||
Common::SettingsHandler handler(std::move(buffer));
|
||||
ASSERT_EQ(handler.GetValue("key"), "val");
|
||||
|
||||
handler.Reset();
|
||||
EXPECT_EQ(handler.GetValue("key"), "");
|
||||
}
|
||||
|
||||
// TODO: Add test coverage of the edge case fixed in
|
||||
// https://github.com/dolphin-emu/dolphin/pull/8704.
|
|
@ -54,6 +54,7 @@
|
|||
<ClCompile Include="Common\FloatUtilsTest.cpp" />
|
||||
<ClCompile Include="Common\MathUtilTest.cpp" />
|
||||
<ClCompile Include="Common\NandPathsTest.cpp" />
|
||||
<ClCompile Include="Common\SettingsHandlerTest.cpp" />
|
||||
<ClCompile Include="Common\SPSCQueueTest.cpp" />
|
||||
<ClCompile Include="Common\StringUtilTest.cpp" />
|
||||
<ClCompile Include="Common\SwapTest.cpp" />
|
||||
|
|
Loading…
Reference in New Issue