Compare commits

...

17 Commits

Author SHA1 Message Date
Paul Joseph Canavan
9401075498
Merge 0917ad89e0 into 033a0540f7 2025-08-31 16:40:38 -04:00
Tilka
033a0540f7
Merge pull request #13899 from SuperSamus/patch-cheats-osd
PatchEngine: OSD message showing number of enabled patches and cheats
2025-08-31 00:27:23 +01:00
Martino Fontana
64a20c74fc PatchEngine: OSD message showing number of enabled patches and cheats 2025-08-30 16:51:51 +02:00
Tilka
76c114a02b
Merge pull request #13910 from Tilka/ub
Fix some undefined behavior
2025-08-30 06:17:45 +01:00
Tillmann Karras
c248f1afa4 IOS/WD: fix wrong BitSet ctor being called
We don't want list initialization here.

Thank you, UB-san.
2025-08-30 03:05:35 +01:00
Tillmann Karras
63257d1ee9 CoreTiming: set default overclock factors to 1.0
I'm not aware of any issue here but let's set meaningful defaults
anyway.
2025-08-30 03:05:35 +01:00
Tillmann Karras
18f0bd1d4b VI: set default overclock factor to 1.0
GetTicksPerHalfLine() gets called via Preset() before RefreshConfig()
has a chance to replace the dummy value 0.0.

Thank you, UB-san.
2025-08-30 03:03:21 +01:00
Tilka
2ff3a7215b
Merge pull request #13902 from Tilka/fix_color_clamp
VideoSW: fix clamping after vertex color interpolation
2025-08-30 03:02:42 +01:00
Tilka
b47a75fa2d
Merge pull request #13912 from jordan-woyak/simplify-saturating-cast
MathUtil: Simplify SaturatingCast implementation and fix edge case.
2025-08-30 03:00:15 +01:00
Tilka
25be1cfe97
Merge pull request #13911 from Dentomologist/gamelist_gridview_sorting
GameList: Use List View's sorting for Grid View
2025-08-30 02:56:18 +01:00
JMC47
e0c72cd963
Merge pull request #13825 from jordan-woyak/dont-count-playtime-while-suspended
Common/Timer: Add a SteadyAwakeClock class to make play time tracking ignore time while suspended.
2025-08-28 13:58:26 -04:00
Jordan Woyak
da546bebb8 MathUtil: Simplify SaturatingCast implementation with std::cmp_less/cmp_greater and fix a floating point edge case.
Thanks to Dentomologist for catching the edge case.
2025-08-27 21:24:46 -05:00
Dentomologist
489fd643d3 GameList: Use List View's sorting for Grid View
Since Grid View doesn't have a header for users to change sorting
options with, use List View's sorting in Grid View too.
2025-08-27 15:35:38 -07:00
Tillmann Karras
bc417bdcee VideoSW: fix clamping after vertex color interpolation 2025-08-24 02:47:56 +01:00
Motobug
0917ad89e0 ini changes for S&K fix 2025-08-05 11:48:46 -04:00
Jordan Woyak
81ebf45c9b Core: Make play time tracking use SteadyAwakeClock. 2025-07-24 23:56:10 -05:00
Jordan Woyak
62c773ac75 Common/Timer: Add a SteadyAwakeClock class which counts non-suspended system running time. 2025-07-24 23:56:10 -05:00
19 changed files with 145 additions and 38 deletions

View File

@ -2,6 +2,8 @@
[Core]
# Values set here will override the main Dolphin settings.
Overclock = 1.4
OverclockEnable = True
[OnFrame]
# Add memory patches to be applied every frame here.

View File

@ -1,5 +1,10 @@
# MCDE8P - Sonic & Knuckles
[Core]
# Values set here will override the main Dolphin settings.
Overclock = 1.2
OverclockEnable = True
[Video_Settings]
[Video_Hacks]

View File

@ -6,8 +6,10 @@
#include <algorithm>
#include <bit>
#include <cmath>
#include <concepts>
#include <limits>
#include <type_traits>
#include <utility>
#include "Common/CommonTypes.h"
@ -31,41 +33,24 @@ constexpr auto Lerp(const T& x, const T& y, const F& a) -> decltype(x + (y - x)
// Casts the specified value to a Dest. The value will be clamped to fit in the destination type.
// Warning: The result of SaturatingCast(NaN) is undefined.
template <typename Dest, typename T>
template <std::integral Dest, typename T>
constexpr Dest SaturatingCast(T value)
{
static_assert(std::is_integral<Dest>());
[[maybe_unused]] constexpr Dest lo = std::numeric_limits<Dest>::lowest();
constexpr Dest lo = std::numeric_limits<Dest>::min();
constexpr Dest hi = std::numeric_limits<Dest>::max();
// T being a signed integer and Dest unsigned is a problematic case because the value will
// be converted into an unsigned integer, and u32(...) < 0 is always false.
if constexpr (std::is_integral<T>() && std::is_signed<T>() && std::is_unsigned<Dest>())
if constexpr (std::is_integral_v<T>)
{
static_assert(lo == 0);
if (value < 0)
if (std::cmp_less(value, lo))
return lo;
// Now that we got rid of negative values, we can safely cast value to an unsigned T
// since unsigned T can represent any positive value signed T could represent.
// The compiler will then promote the LHS or the RHS if necessary.
if (std::make_unsigned_t<T>(value) > hi)
return hi;
}
else if constexpr (std::is_integral<T>() && std::is_unsigned<T>() && std::is_signed<Dest>())
{
// value and hi will never be negative, and hi is representable as an unsigned Dest.
if (value > std::make_unsigned_t<Dest>(hi))
if (std::cmp_greater(value, hi))
return hi;
}
else
{
// Do not use std::clamp or a similar function here to avoid overflow.
// For example, if Dest = s64 and T = int, we want integer promotion to convert value to a s64
// instead of changing lo or hi into an int.
if (value < lo)
if (value < static_cast<T>(lo))
return lo;
if (value > hi)
if (value >= static_cast<T>(hi))
return hi;
}
return static_cast<Dest>(value);

View File

@ -5,6 +5,7 @@
#include <chrono>
#include <thread>
#include "Common/CommonFuncs.h"
#ifdef _WIN32
#include <Windows.h>
@ -194,4 +195,37 @@ void PrecisionTimer::SleepUntil(Clock::time_point target)
}
}
// Results are appropriately slewed on Linux, but not on Windows, macOS, or FreeBSD.
// Clocks with that functionality seem to not be available there.
auto SteadyAwakeClock::now() -> time_point
{
#if defined(_WIN32)
// The count is system time "in units of 100 nanoseconds".
using InterruptDuration = std::chrono::duration<ULONGLONG, std::ratio<100, std::nano::den>::type>;
ULONGLONG interrupt_time{};
if (!QueryUnbiasedInterruptTime(&interrupt_time))
ERROR_LOG_FMT(COMMON, "QueryUnbiasedInterruptTime");
return time_point{InterruptDuration{interrupt_time}};
#else
// Note that Linux's CLOCK_MONOTONIC "does not count time that the system is suspended".
// This is in contrast to the behavior on macOS and FreeBSD.
static constexpr auto clock_id =
#if defined(__linux__)
CLOCK_MONOTONIC;
#elif defined(__APPLE__)
CLOCK_UPTIME_RAW;
#else
CLOCK_UPTIME;
#endif
timespec ts{};
if (clock_gettime(clock_id, &ts) != 0)
ERROR_LOG_FMT(COMMON, "clock_gettime: {}", LastStrerrorString());
return time_point{std::chrono::seconds{ts.tv_sec} + std::chrono::nanoseconds{ts.tv_nsec}};
#endif
}
} // Namespace Common

View File

@ -50,4 +50,19 @@ private:
#endif
};
// Similar to std::chrono::steady_clock except this clock
// specifically does *not* count time while the system is suspended.
class SteadyAwakeClock
{
public:
using rep = s64;
using period = std::nano;
using duration = std::chrono::duration<rep, period>;
using time_point = std::chrono::time_point<SteadyAwakeClock>;
static constexpr bool is_steady = true;
static time_point now();
};
} // Namespace Common

View File

@ -174,6 +174,15 @@ void AddCode(ARCode code)
}
}
size_t CountEnabledCodes()
{
if (!Config::AreCheatsEnabled())
return 0;
std::lock_guard guard(s_lock);
return s_active_codes.size();
}
void LoadAndApplyCodes(const Common::IniFile& global_ini, const Common::IniFile& local_ini,
const std::string& game_id, u16 revision)
{

View File

@ -50,6 +50,7 @@ void SetSyncedCodesAsActive();
void UpdateSyncedCodes(std::span<const ARCode> codes);
std::vector<ARCode> ApplyAndReturnCodes(std::span<const ARCode> codes);
void AddCode(ARCode new_code);
size_t CountEnabledCodes();
void LoadAndApplyCodes(const Common::IniFile& global_ini, const Common::IniFile& local_ini,
const std::string& game_id, u16 revision);

View File

@ -203,8 +203,8 @@ private:
EventType* m_ev_lost = nullptr;
CPUThreadConfigCallback::ConfigChangedCallbackID m_registered_config_callback_id;
float m_config_oc_factor = 0.0f;
float m_config_oc_inv_factor = 0.0f;
float m_config_oc_factor = 1.0f;
float m_config_oc_inv_factor = 1.0f;
bool m_config_sync_on_skip_idle = false;
s64 m_throttle_reference_cycle = 0;

View File

@ -50,6 +50,15 @@ static std::vector<GeckoCode> s_active_codes;
static std::vector<GeckoCode> s_synced_codes;
static std::mutex s_active_codes_lock;
size_t CountEnabledCodes()
{
if (!Config::AreCheatsEnabled())
return 0;
std::lock_guard guard(s_active_codes_lock);
return s_active_codes.size();
}
void SetActiveCodes(std::span<const GeckoCode> gcodes, const std::string& game_id, u16 revision)
{
std::lock_guard lk(s_active_codes_lock);

View File

@ -60,6 +60,7 @@ constexpr u32 HLE_TRAMPOLINE_ADDRESS = INSTALLER_END_ADDRESS - 4;
// preserve the emulation performance.
constexpr u32 MAGIC_GAMEID = 0xD01F1BAD;
size_t CountEnabledCodes();
void SetActiveCodes(std::span<const GeckoCode> gcodes, const std::string& game_id, u16 revision);
void SetSyncedCodesAsActive();
void UpdateSyncedCodes(std::span<const GeckoCode> gcodes);

View File

@ -72,8 +72,8 @@ void CPUManager::StartTimePlayedTimer()
{
Common::SetCurrentThreadName("Play Time Tracker");
// Steady clock for greater accuracy of timing
std::chrono::steady_clock timer;
// Use a clock that will appropriately ignore suspended system time.
Common::SteadyAwakeClock timer;
auto prev_time = timer.now();
while (true)

View File

@ -454,7 +454,7 @@ private:
u32 m_even_field_last_hl = 0; // index last halfline of the even field
u32 m_odd_field_last_hl = 0; // index last halfline of the odd field
float m_config_vi_oc_factor = 0.0f;
float m_config_vi_oc_factor = 1.0f;
Config::ConfigChangedCallbackID m_config_changed_callback_id;
Core::System& m_system;

View File

@ -28,7 +28,7 @@ constexpr u16 LegalNitroChannelMask = 0b0011'1111'1111'1110u;
u16 SelectWifiChannel(u16 enabled_channels_mask, u16 current_channel)
{
const Common::BitSet<u16> enabled_channels{enabled_channels_mask & LegalChannelMask};
const Common::BitSet<u16> enabled_channels(enabled_channels_mask & LegalChannelMask);
u16 next_channel = current_channel;
for (int i = 0; i < 16; ++i)
{

View File

@ -35,6 +35,7 @@
#include "Core/PowerPC/MMU.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include "VideoCommon/OnScreenDisplay.h"
namespace PatchEngine
{
@ -200,6 +201,18 @@ void LoadPatches()
ActionReplay::LoadAndApplyCodes(globalIni, localIni, sconfig.GetGameID(),
sconfig.GetRevision());
}
const size_t enabled_patch_count =
std::ranges::count_if(s_on_frame, [](Patch patch) { return patch.enabled; });
if (enabled_patch_count > 0)
{
OSD::AddMessage(fmt::format("{} game patch(es) enabled", enabled_patch_count),
OSD::Duration::NORMAL);
}
const size_t enabled_cheat_count = ActionReplay::CountEnabledCodes() + Gecko::CountEnabledCodes();
if (enabled_cheat_count > 0)
OSD::AddMessage(fmt::format("{} cheat(s) enabled", enabled_cheat_count), OSD::Duration::NORMAL);
}
static void ApplyPatches(const Core::CPUThreadGuard& guard, const std::vector<Patch>& patches)

View File

@ -122,12 +122,19 @@ GameList::GameList(QWidget* parent) : QStackedWidget(parent), m_model(this)
m_list_proxy->setSortRole(GameListModel::SORT_ROLE);
m_list_proxy->setSourceModel(&m_model);
m_grid_proxy = new GridProxyModel(this);
m_grid_proxy->setSortCaseSensitivity(Qt::CaseInsensitive);
m_grid_proxy->setSortRole(GameListModel::SORT_ROLE);
m_grid_proxy->setSourceModel(&m_model);
MakeListView();
MakeGridView();
MakeEmptyView();
// Use List View's sorting for Grid View too.
m_grid_proxy->sort(m_list_proxy->sortColumn(), m_list_proxy->sortOrder());
connect(m_list->horizontalHeader(), &QHeaderView::sortIndicatorChanged, m_grid_proxy,
&GridProxyModel::sort);
if (Settings::GetQSettings().contains(QStringLiteral("gridview/scale")))
m_model.SetScale(Settings::GetQSettings().value(QStringLiteral("gridview/scale")).toFloat());

View File

@ -18,8 +18,7 @@ const QSize LARGE_BANNER_SIZE(144, 48);
GridProxyModel::GridProxyModel(QObject* parent) : QSortFilterProxyModel(parent)
{
setSortCaseSensitivity(Qt::CaseInsensitive);
sort(static_cast<int>(GameListModel::Column::Title));
setDynamicSortFilter(true);
}
QVariant GridProxyModel::data(const QModelIndex& i, int role) const
@ -76,3 +75,24 @@ bool GridProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_
GameListModel* glm = qobject_cast<GameListModel*>(sourceModel());
return glm->ShouldDisplayGameListItem(source_row);
}
bool GridProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
if (left.data(GameListModel::SORT_ROLE) != right.data(GameListModel::SORT_ROLE))
return QSortFilterProxyModel::lessThan(left, right);
// If two items are otherwise equal, compare them by their title
const auto right_title = sourceModel()
->index(right.row(), static_cast<int>(GameListModel::Column::Title))
.data()
.toString();
const auto left_title = sourceModel()
->index(left.row(), static_cast<int>(GameListModel::Column::Title))
.data()
.toString();
if (sortOrder() == Qt::AscendingOrder)
return left_title < right_title;
return right_title < left_title;
}

View File

@ -14,5 +14,8 @@ class GridProxyModel final : public QSortFilterProxyModel
public:
explicit GridProxyModel(QObject* parent = nullptr);
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
protected:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
};

View File

@ -173,12 +173,8 @@ static void Draw(s32 x, s32 y, s32 xi, s32 yi)
{
for (int comp = 0; comp < 4; comp++)
{
u16 color = (u16)ColorSlopes[i][comp].GetValue(x, y);
// clamp color value to 0
u16 mask = ~(color >> 8);
tev.Color[i][comp] = color & mask;
const float color = ColorSlopes[i][comp].GetValue(x, y);
tev.Color[i][comp] = (u8)std::clamp<float>(color, 0.0f, 255.0f);
}
}

View File

@ -66,6 +66,13 @@ TEST(MathUtil, SaturatingCast)
// 16777217 = 2^24 + 1 is the first integer that cannot be represented correctly with a f32.
EXPECT_EQ(16777216, MathUtil::SaturatingCast<s32>(float(16777216)));
EXPECT_EQ(16777216, MathUtil::SaturatingCast<s32>(float(16777217)));
// Note that values in the range [2147483584, 2147483776] have an equivalent float representation.
EXPECT_EQ(std::numeric_limits<s32>::max(), MathUtil::SaturatingCast<s32>(2147483648.f));
EXPECT_EQ(std::numeric_limits<s32>::min(), MathUtil::SaturatingCast<s32>(-2147483649.f));
// Cast from a signed integer type to a smaller signed integer type
EXPECT_EQ(-128, (MathUtil::SaturatingCast<s8, int>(-129)));
}
TEST(MathUtil, RectangleEquality)