Compare commits

...

59 Commits

Author SHA1 Message Date
AGuy27 eca6072ccb
Merge branch 'dolphin-emu:master' into AGuy27-patch-2 2024-03-05 16:45:11 -05:00
OatmealDome e7282f4244
Merge pull request #12618 from LillyJadeKatrin/retroachievements-update-fix
Remove the update callback on all events
2024-03-04 21:02:45 -05:00
LillyJadeKatrin 085c17aed4 Remove the update callback on all events
Not only was the extra call to the update callback in the AchievementEventHandler method unnecessary, it was getting called on events that don't even need to be tracked here, causing a lot of lag when it turned out one achievement was repeatedly spamming Achievement Reset Events as a shortcut.
2024-03-04 20:04:08 -05:00
OatmealDome 8f6fd912f7
Merge pull request #12613 from LillyJadeKatrin/retroachievements-submodule-update
Update rcheevos submodule to master
2024-03-03 20:09:37 -05:00
OatmealDome 6372bf51eb
Merge pull request #12597 from nlebeck/settingshandler-unittests-thirdtry
Add some simple unit tests for SettingsHandler
2024-03-03 18:13:45 -05:00
LillyJadeKatrin a2db33b6f1 Update rcheevos submodule to master 2024-03-03 17:55:47 -05:00
OatmealDome aba9b110e8
Merge pull request #12612 from LillyJadeKatrin/retroachievements-logout-bug
Don't Publish Unofficial Achievements
2024-03-03 14:51:04 -05:00
JMC47 93c95ee1a3
Merge pull request #12592 from LillyJadeKatrin/retroachievements-enable-hardcore
Properly enable RetroAchievements hardcore mode
2024-03-03 13:20:23 -05:00
LillyJadeKatrin f556f646a2 Don't Publish Unofficial Achievements
Check an achievement's category and only request RetroAchievements to award if it is CORE and not UNOFFICIAL.
2024-03-03 12:58:20 -05:00
Admiral H. Curtiss 58616a6e4c
Merge pull request #12438 from Filoppi/custom_relative_aspect_ratio
Add Custom Relative and Raw (Squared Pixels) aspect ratios
2024-03-02 14:14:19 +01:00
Admiral H. Curtiss f2e04e0603
Merge pull request #12359 from mitaclaw/code-diff-dialog-refresh
BranchWatchDialog: A Total Replacement for CodeDiffDialog
2024-03-02 14:13:54 +01:00
Admiral H. Curtiss 8840f7f8bc
Merge pull request #12599 from lioncash/vertman
VertexManagerBase: Initialize m_ticks_elapsed on construction
2024-03-02 14:11:23 +01:00
Admiral H. Curtiss 349e21aa3a
Merge pull request #12607 from Naim2000/devaes
AesDevice: fix key & iv arrays
2024-03-02 14:08:24 +01:00
Niel Lebeck 0344ec6d79 Add simple unit tests for SettingsHandler 2024-03-01 20:52:45 -08:00
Naim2000 f208b320a3 AesDevice: fix key & iv arrays 2024-03-01 15:00:28 -05:00
Filoppi 41b19e262f Add custom relative and raw (squared pixels) aspect ratio modes 2024-02-29 21:11:19 +02:00
Lioncash 7bf77a56f4 VertexManagerBase: Initialize m_ticks_elapsed on construction
Ensures that this always has a deterministic value on construction like
everything else in the class.
2024-02-28 10:21:08 -05:00
Mai 5a81916ee9
Merge pull request #12598 from mitaclaw/case-insensitive
CodeWidget: Simplify Case-Insensitive Contains
2024-02-28 08:48:10 -05:00
mitaclaw 8eeb93d51a CodeWidget: Simplify Case-Insensitive Contains 2024-02-27 12:03:38 -08:00
mitaclaw 8134c8a572 BranchWatchDialog: A Total Replacement for CodeDiffDialog
With a purpose-built Branch Watch feature built into the emulated system: BranchWatchDialog, replacing CodeDiffDialog, is now better than ever!
2024-02-27 11:40:58 -08:00
Mai aae61894f8
Merge pull request #12595 from LillyJadeKatrin/retroachievements-bugfix-2
Clear active challenges when game closes
2024-02-27 02:30:34 -05:00
mitaclaw fd8f2c7822 JitArm64: Install BranchWatch 2024-02-26 19:38:27 -08:00
mitaclaw 7cccedca1e Jit64: Install BranchWatch 2024-02-26 19:38:27 -08:00
mitaclaw 2aa250a68a Interpreter: Install BranchWatch 2024-02-26 19:38:27 -08:00
mitaclaw 67f60bec7e PowerPC: Implement BranchWatch
This new component can track code paths by watching branch hits.
2024-02-26 19:38:27 -08:00
LillyJadeKatrin 56b82e764c Clear active challenges when game closes
Failing to do this was causing challenge icons to carry over into the next game if a game was closed while the icons were active, even if the next game to run was a completely different game entirely with completely different badges.
2024-02-26 22:21:05 -05:00
Admiral H. Curtiss 5090a028e6
Merge pull request #12435 from Filoppi/fix-aspect-ratio-stuck
Fix aspect ratio heuristics getting stuck to a state
2024-02-20 21:36:43 +01:00
Admiral H. Curtiss 4b56ce3988
Merge pull request #12437 from Filoppi/improve-window-presentation-resolution
Improve window presentation at native resolution
2024-02-20 21:28:10 +01:00
Filoppi 48fbbdba7c Video: update widescreen heuristic code to never get stuck to specific old values when changing settings 2024-02-20 22:26:19 +02:00
Admiral H. Curtiss 3a41d991ce
Merge pull request #12591 from JesseTG/jtg/cmake-flexibility
Have dolphin_scmrev run `ScmRevGen` from `CMAKE_CURRENT_SOURCE_DIR`
2024-02-20 20:34:14 +01:00
LillyJadeKatrin d66b96b1c0 Properly enable RetroAchievements hardcore mode
RetroAchievements plans to use the user_agent in unlock requests to determine which software version was used to play the game, and can filter older software versions out. As such, I have been given the go-ahead to remove the hardcoded line that forces hardcore to always be false.
2024-02-20 12:59:01 -05:00
Jesse Talavera a84dc3123e Use `CMAKE_CURRENT_SOURCE_DIR` for the CMake module path 2024-02-20 08:09:06 -05:00
Filoppi 3f102ea8c2 Video: Make the game resolution (within the window) snap to the XFB size if they are within a ~1 pixel treshold on one axis only.
This takes care of making the image clearer in some edge cases where the game was already running at near perfect
 4:3 with no stretching, and the VI aspect ratio didn't match the XFB by one pixel, making the image stretched and blurry.
-Video: Fix `FindClosestIntegerResolution() using the window aspect ratio and not the draw aspect ratio, causing it to prefer
 stretching over black bars in cases when it wasn't desirable.
2024-02-20 03:09:11 +02:00
Filoppi 95ee0ac781 Video: Fix aspect ratio heuristics getting stuck to widescreen (or to non widescreen) (`m_is_game_widescreen` variable) if the user first forced the aspect ratio to 16:9/4:3 and then set it back to Auto. 2024-02-20 02:42:52 +02:00
Jesse Talavera edaafaae2f Have dolphin_scmrev run `ScmRevGen` from `CMAKE_CURRENT_SOURCE_DIR` 2024-02-19 11:41:01 -05:00
Admiral H. Curtiss ccf2435047
Merge pull request #12586 from LillyJadeKatrin/retroachievements-pointspread-fix
Fixes to Achievement points count/mastery
2024-02-19 02:41:14 +01:00
LillyJadeKatrin e5b73fec08 Fixes to Achievement points count/mastery
Two minor updates to improve the Achievement Manager's handling of a player's completion rate.
One, UnlockStatus and the unlock map now track achievement category, such that TallyScore does not count unofficial achievements in counts/points.
Two, the determinations for mastery/completion are now improved to check (1) that the achievement triggering this is CORE (not UNOFFICIAL) and (2) that it has not already been unlocked at this level on the site, which should be sufficient to determine that the unlocking of this particular achievement completes/masters the game.
2024-02-18 19:27:18 -05:00
Mai 22d96ef5b5
Merge pull request #12590 from iwubcode/graphics_mod_action_factory_name
VideoCommon: move factory names to be a static inside each action class
2024-02-18 17:13:03 -05:00
Mai 27415b0ba1
Merge pull request #12587 from AdmiralCurtiss/localtime
Core: Fix crash when inspecting a savestate with a timestamp that causes localtime() to error out
2024-02-18 17:12:29 -05:00
Mai cd9a7db4c1
Merge pull request #12589 from AdmiralCurtiss/ios-fs-logs
IOS/FS: Display the invalid path in the ASSERT in BuildFilename().
2024-02-18 17:11:53 -05:00
iwubcode a1147dae6e VideoCommon: move factory names to be a static inside each action class, so that they can be reused in the future for serialization 2024-02-18 15:45:10 -06:00
Admiral H. Curtiss 638808c944
IOS/FS: Display the invalid path in the ASSERT in BuildFilename(). 2024-02-18 20:06:32 +01:00
Admiral H. Curtiss 0157166940
Merge pull request #12585 from iwubcode/json_util_vec3
Common: add json utility functions for Vec3 serialization
2024-02-18 19:02:59 +01:00
iwubcode edbf8f1772 Common: add json utility functions for Vec3 serialization 2024-02-17 22:06:06 -06:00
Admiral H. Curtiss d3140e72c3
Core: Fix crash when inspecting a savestate with a timestamp that causes localtime() to error out. 2024-02-18 04:45:37 +01:00
Admiral H. Curtiss 52410813f2
Common: Add utility function that wraps localtime_s() or localtime_t(). 2024-02-18 04:40:25 +01:00
Admiral H. Curtiss 982ad93355
Merge pull request #12582 from LillyJadeKatrin/retroachievements-bugfix-2
Improved achievements disabled messaging
2024-02-18 03:49:44 +01:00
Admiral H. Curtiss 7d6a5d3665
Merge pull request #12583 from JesseTG/jtg/cmake-flexibility
Simplify including Dolphin via `FetchContent`
2024-02-18 03:06:03 +01:00
Jesse Talavera acb18a58cf Make the path given to `CMAKE_USER_MAKE_RULES_OVERRIDE` absolute 2024-02-17 20:36:17 -05:00
Admiral H. Curtiss 9b5fd5d34e
Merge pull request #12281 from TellowKrinkle/AsahiGL33
VideoCommon: Don't use indexed output for fbfetch
2024-02-18 02:33:50 +01:00
Admiral H. Curtiss b30d6e92db
Merge pull request #12314 from Dentomologist/balloontip_rework
Balloontip drawing rework
2024-02-18 00:35:18 +01:00
Dentomologist 56ff19c513 BalloonTip: Rework BalloonTip drawing
* Fix irregularly shaped corners
* Remove extra space for BalloonTips with no message or no title
* When the target tip location is not on a screen, put the tooltip on
  the mouse's screen instead of the primary screen
* Fix description getting cut off when the title was too long
* Expose border width as a parameter
* Fix spacing and sizing issues with larger border widths
2024-02-17 12:36:19 -08:00
Mai 21300bb21b
Merge pull request #12457 from iwubcode/asset_memory_limit
VideoCommon: handle asset memory going over reserved limit correctly
2024-02-16 15:46:52 -05:00
Mai 946aa45abd
Merge pull request #12558 from AGuy27/patch-1
Create Strike Force Bowling INI
2024-02-16 15:44:33 -05:00
LillyJadeKatrin 394af40db5 Improved achievements disabled messaging
Most obviously, there is no longer a warning message to the player in the achievement window that achievements are disabled if a game is not currently running.
2024-02-15 16:33:18 -05:00
TellowKrinkle 5949911a5a VideoCommon: Don't use indexed output for fbfetch
A nonzero index makes no sense, and Mesa doesn't like it when you supply an index
2024-02-07 03:52:31 +01:00
AGuy27 573102d4cf
Update and rename G5BE4Z.ini to G5B.ini 2024-02-03 15:32:13 -05:00
AGuy27 3ae535fa05
Create G5BE4Z.ini
Fixes Strike Force Bowling blank texture
2024-02-03 00:35:16 -05:00
iwubcode b669580aeb VideoCommon: handle asset memory going over reserved limit correctly by erroring when the memory is exceeded and not allowing more assets to load until memory is released 2023-12-21 01:05:56 -06:00
79 changed files with 3734 additions and 1144 deletions

View File

@ -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
)

View File

@ -0,0 +1,4 @@
# G5BE4Z - Strike Force Bowling
[Video_Hacks]
EFBToTextureEnable = False
DeferEFBCopies = False

View File

@ -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

View File

@ -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>

View File

@ -138,6 +138,8 @@ add_library(common
Thread.h
Timer.cpp
Timer.h
TimeUtil.cpp
TimeUtil.h
TraversalClient.cpp
TraversalClient.h
TraversalProto.h

View File

@ -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"

View File

@ -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;

View File

@ -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,

View File

@ -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");
}

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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";

View File

@ -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;
}

View File

@ -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

View File

@ -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")

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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());

View File

@ -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};
}

View File

@ -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)
{
}

View File

@ -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;

View File

@ -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;

View File

@ -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)
{

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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)
{

View File

@ -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,

View File

@ -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)

View File

@ -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);
}

View File

@ -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())

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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)
{
}

View File

@ -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

View File

@ -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" />

View File

@ -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());
}
}

View File

@ -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

View File

@ -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 "

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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

View File

@ -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;
};

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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()

View File

@ -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;

View File

@ -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" />

View File

@ -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");

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -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));
}

View File

@ -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)
{

View File

@ -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;
}

View File

@ -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;

View File

@ -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)
{

View File

@ -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;

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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)

View File

@ -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.

View File

@ -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" />