This commit is contained in:
Jordan Woyak 2025-12-15 17:14:34 -06:00 committed by GitHub
commit 03f2dc4036
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 486 additions and 34 deletions

View File

@ -128,6 +128,7 @@ add_library(common
QoSSession.h
Random.cpp
Random.h
Rational.h
Result.h
ScopeGuard.h
SDCardUtil.cpp

View File

@ -175,4 +175,15 @@ constexpr int IntLog2(u64 val)
{
return 63 - std::countl_zero(val);
}
// Similar to operator<=> but negative signed values always compare less than unsigned values.
constexpr auto Compare3Way(std::integral auto lhs, std::integral auto rhs)
{
if (std::cmp_less(lhs, rhs))
return std::strong_ordering::less;
if (std::cmp_less(rhs, lhs))
return std::strong_ordering::greater;
return std::strong_ordering::equal;
}
} // namespace MathUtil

View File

@ -0,0 +1,312 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cmath>
#include <concepts>
#include <limits>
#include <numeric>
#include <utility>
#include "Common/MathUtil.h"
namespace MathUtil
{
template <std::integral T>
class Rational;
namespace detail
{
template <typename T>
concept RationalCompatible = requires(T x) { Rational(x); };
template <typename T>
concept RationalAdjacent = std::is_arithmetic_v<T>;
// Allows for class template argument deduction within the class body.
// Note: CTAD via alias template would be cleaner but was not implemented until Clang 19.
template <typename Other>
constexpr auto DeducedRational(Other other)
{
return Rational{other};
}
} // namespace detail
// Rational number class template
// Results are not currently automatically reduced.
// Watch out for integer overflow after repeated operations.
// Use Reduced / Approximated functions as needed to avoid overflow.
template <std::integral T>
class Rational final
{
public:
using ValueType = T;
ValueType numerator{0};
ValueType denominator{1};
// Conversion from integers
constexpr Rational(T num = 0, T den = 1) : numerator{num}, denominator{den} {}
// Conversion from floating point
// Properly converts nice values like 0.5f -> Rational(1, 2)
// Inexact values like 0.3f won't convert particularly nicely.
// But e.g. Rational(0.3f).Approximated(10) will produce Rational(3, 10)
constexpr explicit Rational(std::floating_point auto float_value)
{
constexpr int dest_exp = IntLog2(std::numeric_limits<T>::max()) - 1;
int exp = 0;
const auto norm_frac = std::frexp(float_value, &exp);
const int num_exp = std::max(exp, dest_exp);
numerator = SaturatingCast<T>(std::ldexp(norm_frac, num_exp));
const int zeros = std::countr_zero(std::make_unsigned_t<T>(numerator));
const int den_exp = num_exp - exp;
const int shift_right = std::min(den_exp, zeros);
denominator = SaturatingCast<T>(std::ldexp(1, den_exp - shift_right));
numerator >>= shift_right;
}
// Conversion from other Rational.
template <std::integral Other>
constexpr explicit Rational(Rational<Other> other)
{
if constexpr (std::is_unsigned_v<T> && std::is_signed_v<Other>)
other = other.Normalized();
numerator = SaturatingCast<T>(other.numerator);
denominator = SaturatingCast<T>(other.denominator);
}
// Potentially lossy conversion to int/float types
template <detail::RationalAdjacent Target>
constexpr explicit operator Target() const
{
return Target(numerator) / Target(denominator);
}
constexpr bool IsInteger() const { return (numerator % denominator) == 0; }
constexpr Rational Inverted() const { return Rational(denominator, numerator); }
// Returns a copy with a non-negative denominator.
constexpr Rational Normalized() const
{
return (denominator < T{0}) ? Rational(T{} - numerator, T{} - denominator) : *this;
}
// Returns a reduced fraction.
constexpr Rational Reduced() const
{
const auto gcd = std::gcd(numerator, denominator);
return {T(numerator / gcd), T(denominator / gcd)};
}
// Returns a reduced approximated fraction with a given maximum numerator/denominator.
constexpr Rational Approximated(T max_num_den) const
{
// This algorithm comes from FFmpeg's av_reduce function (LGPLv2.1+)
const auto reduced_normalized = Reduced().Normalized();
auto [num, den] = reduced_normalized;
const bool is_negative = num < T{0};
if (is_negative)
num *= -1;
if (num <= max_num_den && den <= max_num_den)
return reduced_normalized;
Rational a0{0, 1};
Rational a1{1, 0};
while (den != 0)
{
auto x = num / den;
const auto next_den = num - (den * x);
const Rational a2 = (Rational(x, x) * a1) & a0;
if (a2.numerator > max_num_den || a2.denominator > max_num_den)
{
if (a1.numerator != 0)
x = (max_num_den - a0.numerator) / a1.numerator;
if (a1.denominator != 0)
x = std::min(x, (max_num_den - a0.denominator) / a1.denominator);
if (den * (2 * x * a1.denominator + a0.denominator) > num * a1.denominator)
a1 = {(Rational(x, x) * a1) & a0};
break;
}
a0 = a1;
a1 = a2;
num = den;
den = next_den;
}
return is_negative ? -a1 : a1;
}
// Multiplication
constexpr auto& operator*=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
numerator *= r.numerator;
denominator *= r.denominator;
return *this;
}
constexpr friend auto operator*(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) *= rhs;
}
constexpr friend auto operator*(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs * CommonRational(rhs);
}
// Division
constexpr auto& operator/=(detail::RationalCompatible auto rhs)
{
return *this *= CommonRational(rhs).Inverted();
}
constexpr friend auto operator/(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) *= rhs.Inverted();
}
constexpr friend auto operator/(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs * CommonRational(rhs).Inverted();
}
// Modulo (behaves like fmod)
constexpr auto& operator%=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
return *this -= (r * T(*this / r));
}
constexpr friend auto operator%(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) %= rhs;
}
constexpr friend auto operator%(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs % CommonRational(rhs);
}
// Addition
constexpr auto& operator+=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
numerator *= r.denominator;
numerator += r.numerator * denominator;
denominator *= r.denominator;
return *this;
}
constexpr friend auto operator+(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) += rhs;
}
constexpr friend auto operator+(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs + CommonRational(rhs);
}
// Subtraction
constexpr auto& operator-=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
numerator *= r.denominator;
numerator -= r.numerator * denominator;
denominator *= r.denominator;
return *this;
}
constexpr friend auto operator-(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) -= rhs;
}
constexpr friend auto operator-(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs - CommonRational(rhs);
}
// Mediant (n1+n2)/(d1+d2)
constexpr auto& operator&=(detail::RationalCompatible auto rhs)
{
const auto r = CommonRational(rhs);
numerator += r.numerator;
denominator += r.denominator;
return *this;
}
constexpr friend auto operator&(detail::RationalCompatible auto lhs, Rational rhs)
{
return CommonRational(lhs) &= rhs;
}
constexpr friend auto operator&(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs & CommonRational(rhs);
}
// Comparison
constexpr friend auto operator<=>(detail::RationalCompatible auto lhs, Rational rhs)
{
const auto left = detail::DeducedRational(lhs).Normalized();
const auto lhs_q = left.numerator / left.denominator;
const auto lhs_r = left.numerator % left.denominator;
rhs = rhs.Normalized();
const auto rhs_q = rhs.numerator / rhs.denominator;
const auto rhs_r = rhs.numerator % rhs.denominator;
// If integer division results differ we have a result.
if (const auto cmp = Compare3Way(lhs_q, rhs_q); std::is_neq(cmp))
return cmp;
// If at least one side has no remainder we have a result.
if (lhs_r == 0 || rhs_r == 0)
return Compare3Way(lhs_r, rhs_r);
// Recurse with inverted remainders.
return Rational(rhs.denominator, rhs_r) <=> decltype(left)(left.denominator, lhs_r);
}
constexpr friend auto operator<=>(Rational lhs, detail::RationalAdjacent auto rhs)
{
return lhs <=> detail::DeducedRational(rhs);
}
// Equality
constexpr friend bool operator==(detail::RationalCompatible auto lhs, Rational rhs)
{
return std::is_eq(lhs <=> rhs);
}
constexpr friend bool operator==(Rational lhs, detail::RationalAdjacent auto rhs)
{
return std::is_eq(lhs <=> rhs);
}
// Unary operators
constexpr auto operator+() const { return *this; }
constexpr auto operator-() const { return Rational(T{} - numerator, denominator); }
constexpr auto& operator++() { return *this += 1; }
constexpr auto& operator--() { return *this -= 1; }
constexpr auto operator++(int) { return std::exchange(*this, *this + 1); }
constexpr auto operator--(int) { return std::exchange(*this, *this - 1); }
constexpr explicit operator bool() const { return numerator != 0; }
private:
// Constructs a common_type'd Rational<T> from an existing value.
static constexpr auto CommonRational(auto val)
{
return Rational<
std::common_type_t<T, typename decltype(detail::DeducedRational(val))::ValueType>>(val);
}
};
// Floating point deduction guides.
Rational(float) -> Rational<s32>;
Rational(double) -> Rational<s64>;
} // namespace MathUtil

View File

@ -348,8 +348,8 @@ bool AchievementManager::CanPause()
OSD::AddMessage(
fmt::format("RetroAchievements Hardcore Mode:\n"
"Cannot pause until another {:.2f} seconds have passed.",
static_cast<float>(frames_to_next_pause) /
Core::System::GetInstance().GetVideoInterface().GetTargetRefreshRate()),
float(frames_to_next_pause /
Core::System::GetInstance().GetVideoInterface().GetTargetRefreshRate())),
OSD::Duration::VERY_LONG, OSD::Color::RED);
}
return can_pause;

View File

@ -397,8 +397,8 @@ void FifoPlayer::WriteFrame(const FifoFrameInfo& frame, const AnalyzedFrameInfo&
{
// Core timing information
auto& vi = m_system.GetVideoInterface();
m_CyclesPerFrame = static_cast<u64>(m_system.GetSystemTimers().GetTicksPerSecond()) *
vi.GetTargetRefreshRateDenominator() / vi.GetTargetRefreshRateNumerator();
m_CyclesPerFrame =
u64(u64(m_system.GetSystemTimers().GetTicksPerSecond()) / vi.GetTargetRefreshRate());
m_ElapsedCycles = 0;
m_FrameFifoSize = static_cast<u32>(frame.fifoData.size());

View File

@ -81,8 +81,6 @@ void VideoInterfaceManager::DoState(PointerWrap& p)
p.Do(m_fb_width);
p.Do(m_border_hblank);
p.Do(m_target_refresh_rate);
p.Do(m_target_refresh_rate_numerator);
p.Do(m_target_refresh_rate_denominator);
p.Do(m_ticks_last_line_start);
p.Do(m_half_line_count);
p.Do(m_half_line_of_next_si_poll);
@ -734,27 +732,15 @@ void VideoInterfaceManager::UpdateParameters()
void VideoInterfaceManager::UpdateRefreshRate()
{
m_target_refresh_rate_numerator = m_system.GetSystemTimers().GetTicksPerSecond() * 2;
m_target_refresh_rate_denominator = GetTicksPerEvenField() + GetTicksPerOddField();
m_target_refresh_rate =
static_cast<double>(m_target_refresh_rate_numerator) / m_target_refresh_rate_denominator;
m_target_refresh_rate = {m_system.GetSystemTimers().GetTicksPerSecond() * 2,
GetTicksPerEvenField() + GetTicksPerOddField()};
}
double VideoInterfaceManager::GetTargetRefreshRate() const
MathUtil::Rational<u32> VideoInterfaceManager::GetTargetRefreshRate() const
{
return m_target_refresh_rate;
}
u32 VideoInterfaceManager::GetTargetRefreshRateNumerator() const
{
return m_target_refresh_rate_numerator;
}
u32 VideoInterfaceManager::GetTargetRefreshRateDenominator() const
{
return m_target_refresh_rate_denominator;
}
u32 VideoInterfaceManager::GetTicksPerSample() const
{
return 2 * m_system.GetSystemTimers().GetTicksPerSecond() / CLOCK_FREQUENCIES[m_clock & 1];

View File

@ -4,10 +4,10 @@
#pragma once
#include <array>
#include <memory>
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/Rational.h"
enum class FieldType;
class PointerWrap;
@ -381,9 +381,7 @@ public:
// Change values pertaining to video mode
void UpdateParameters();
double GetTargetRefreshRate() const;
u32 GetTargetRefreshRateNumerator() const;
u32 GetTargetRefreshRateDenominator() const;
MathUtil::Rational<u32> GetTargetRefreshRate() const;
u32 GetTicksPerSample() const;
u32 GetTicksPerHalfLine() const;
@ -440,9 +438,7 @@ private:
// 0xcc002076 - 0xcc00207f is full of 0x00FF: unknown
// 0xcc002080 - 0xcc002100 even more unknown
double m_target_refresh_rate = 0;
u32 m_target_refresh_rate_numerator = 0;
u32 m_target_refresh_rate_denominator = 1;
MathUtil::Rational<u32> m_target_refresh_rate{0};
u64 m_ticks_last_line_start = 0; // number of ticks when the current full scanline started
u32 m_half_line_count = 0; // number of halflines that have occurred for this full frame

View File

@ -95,7 +95,7 @@ static size_t s_state_writes_in_queue;
static std::condition_variable s_state_write_queue_is_empty;
// Don't forget to increase this after doing changes on the savestate system
constexpr u32 STATE_VERSION = 175; // Last changed in PR 13751
constexpr u32 STATE_VERSION = 176; // Last changed in PR 14014
// Increase this if the StateExtendedHeader definition changes
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217

View File

@ -155,6 +155,7 @@
<ClInclude Include="Common\Projection.h" />
<ClInclude Include="Common\QoSSession.h" />
<ClInclude Include="Common\Random.h" />
<ClInclude Include="Common\Rational.h" />
<ClInclude Include="Common\Result.h" />
<ClInclude Include="Common\scmrev.h" />
<ClInclude Include="Common\ScopeGuard.h" />

View File

@ -67,11 +67,8 @@ AVRational GetTimeBaseForCurrentRefreshRate(s64 max_denominator)
{
// TODO: GetTargetRefreshRate* are not safe from GPU thread.
auto& vi = Core::System::GetInstance().GetVideoInterface();
int num;
int den;
av_reduce(&num, &den, int(vi.GetTargetRefreshRateDenominator()),
int(vi.GetTargetRefreshRateNumerator()), max_denominator);
return AVRational{num, den};
const auto time_base = vi.GetTargetRefreshRate().Inverted().Approximated(max_denominator);
return AVRational(int(time_base.numerator), int(time_base.denominator));
}
void InitAVCodec()

View File

@ -4,6 +4,7 @@
#include <gtest/gtest.h>
#include "Common/MathUtil.h"
#include "Common/Rational.h"
TEST(MathUtil, IntLog2)
{
@ -18,6 +19,14 @@ TEST(MathUtil, IntLog2)
EXPECT_EQ(63, MathUtil::IntLog2(0xFFFFFFFFFFFFFFFFull));
}
TEST(MathUtil, Compare3Way)
{
EXPECT_TRUE(std::is_lt(MathUtil::Compare3Way(-1, 1u)));
EXPECT_TRUE(std::is_gteq(MathUtil::Compare3Way(5u, -17ll)));
EXPECT_TRUE(std::is_eq(MathUtil::Compare3Way(42ull, 42)));
EXPECT_TRUE(std::is_neq(MathUtil::Compare3Way(s32(-1), u32(-1))));
}
TEST(MathUtil, NextPowerOf2)
{
EXPECT_EQ(4U, MathUtil::NextPowerOf2(3));
@ -194,5 +203,144 @@ TEST(MathUtil, RectangleGetHeightUnsigned)
EXPECT_EQ(rect_e.GetHeight(), u32{0xFFFFFFF8});
}
TEST(MathUtil, Rational)
{
using MathUtil::Rational;
// Integer
const auto r5 = 65 * Rational{8, 13} / 8;
EXPECT_TRUE(r5.IsInteger());
EXPECT_EQ(r5, 5);
EXPECT_EQ(int(r5), 5);
EXPECT_EQ(r5, Rational{10} * 0.5f);
EXPECT_NE(r5, Rational(6));
// Non-Integer
const auto r5_2 = Rational(5, 2);
EXPECT_FALSE(r5_2.IsInteger());
EXPECT_EQ(int(r5_2), 2);
EXPECT_EQ(r5_2, 12.5f / r5);
// True/False
EXPECT_TRUE(r5_2);
EXPECT_FALSE(r5_2 * 0);
EXPECT_FALSE(!r5_2);
// Negative values
EXPECT_EQ(Rational(-4, -3), Rational(4, 3));
EXPECT_EQ(Rational(-1, 10), Rational(1, -10));
EXPECT_TRUE(Rational(-5, 1).IsInteger());
EXPECT_TRUE(Rational(5, -1).IsInteger());
EXPECT_NE(r5, -r5);
// Conversion to/from float
const Rational r3p5(3.5);
EXPECT_EQ(r3p5.numerator, 7);
EXPECT_EQ(r3p5.denominator, 2);
const Rational neg3p5(-3.5);
EXPECT_EQ(neg3p5.numerator, -7);
EXPECT_EQ(neg3p5.denominator, 2);
EXPECT_EQ(float(r5_2), 2.5);
EXPECT_EQ(float(-r5_2), -2.5f);
EXPECT_EQ(r5_2, Rational(2.5f));
EXPECT_EQ(-r5_2, Rational(-2.5));
EXPECT_NE(r5_2, Rational(2.500001f));
// Fraction reduction
const Rational r15_6{15, 6};
EXPECT_EQ(r15_6, r5_2);
const auto f15_6_reduced = r15_6.Reduced();
EXPECT_EQ(f15_6_reduced.numerator, 5);
EXPECT_EQ(f15_6_reduced.denominator, 2);
// Approximations
EXPECT_EQ(Rational(0.3).Approximated(1000'000), Rational(3, 10));
EXPECT_EQ(Rational(3, 10).Approximated(9), Rational(2, 7));
EXPECT_EQ(Rational(-33, 100).Approximated(20), Rational(-1, 3));
EXPECT_EQ(Rational(0.33).Approximated(10), Rational(1, 3));
EXPECT_EQ(Rational(1, -100).Approximated(10), Rational(0, 1));
EXPECT_EQ(Rational(6, -100).Approximated(10), Rational(-1, 10));
EXPECT_EQ(Rational(101).Approximated(20), Rational(20, 1));
EXPECT_EQ(Rational<s16>(std::numeric_limits<s16>::max()).Approximated(18), Rational(18, 1));
constexpr auto s64_max = std::numeric_limits<s64>::max();
EXPECT_EQ(Rational<s64>(-s64_max).Approximated(13), Rational(-13, 1));
EXPECT_EQ(Rational<s64>(1, s64_max).Approximated(s64_max), Rational<s64>(1, s64_max));
// Addition/Subtraction
EXPECT_EQ(-r5_2 + -r15_6, Rational(-5));
EXPECT_EQ(2.5f - -r15_6, Rational(5));
EXPECT_EQ(+r3p5 - 2, 1.5f);
EXPECT_EQ(r3p5 - 7, -3.5);
EXPECT_EQ(r3p5 - u8(2), 1.5);
EXPECT_EQ(r3p5 - 3ull, 0.5);
EXPECT_EQ(r3p5 - Rational<u64>(1), 2.5);
EXPECT_EQ(Rational<u8>(6) - 5.5f, 0.5);
EXPECT_EQ(Rational<u8>(6) + Rational(-5, -5), 7);
EXPECT_EQ(Rational<u8>(6) - Rational<s64>(3, -3), 7);
// Inc/Dec
Rational f7_3_inc{7, 3};
EXPECT_EQ(f7_3_inc++, 2 + Rational(1, 3));
EXPECT_EQ(++f7_3_inc, 4 + Rational(1, 3));
EXPECT_EQ(f7_3_inc--, Rational(1, 3) + 4);
EXPECT_EQ(--f7_3_inc, Rational(1, 3) + 2);
// Multiplication/Division
EXPECT_EQ(r5_2 * 3, Rational(7.5));
EXPECT_EQ(r5_2 * r5_2, 6.25);
EXPECT_EQ(7 / r15_6, 2 + Rational(8, 10));
EXPECT_EQ(r3p5 / r15_6, 12 - Rational(106, 10));
EXPECT_EQ(r3p5 / 2, 1.75);
EXPECT_EQ(int(r3p5 / 2), 1);
EXPECT_EQ(Rational(-1, -3) * 2ull, Rational(2, 3));
auto ru77 = Rational(77u);
ru77 /= Rational(-7, -1);
EXPECT_EQ(ru77, 11);
// Modulo
EXPECT_EQ(r3p5 % 2, 1.5f);
EXPECT_EQ(r3p5 % -2, Rational(3, 2));
EXPECT_EQ(-r3p5 % 2, Rational(3, -2));
EXPECT_EQ(-r3p5 % -2, -1.5f);
// Mediant
EXPECT_EQ(r5_2 & Rational(2, 1), Rational(7, 3));
EXPECT_EQ(Rational(11, 5) & Rational(3, -1), Rational(14, 4));
// Comparison
EXPECT_TRUE(Rational(-5, 101) < 0);
EXPECT_TRUE(Rational(5, -101) < 0u);
EXPECT_TRUE(Rational(-5, -101) > 0);
EXPECT_TRUE(Rational(7, 5) > Rational(7, 6));
EXPECT_TRUE(Rational(1, 3) < Rational(-2, -3));
EXPECT_TRUE(Rational(0.5) != Rational(2, 2));
EXPECT_TRUE(Rational(10, 3) == 3 + Rational(2, 6));
EXPECT_TRUE(3 >= Rational(6, 2));
EXPECT_TRUE(Rational(6, 2) < 4.0);
// Conversions use SaturatingCast
EXPECT_EQ(Rational<u32>(Rational(-5)), 0);
EXPECT_EQ(Rational<u8>(Rational(1, 1000)), Rational(1, 255));
EXPECT_EQ(Rational<u32>(-9.f), 0);
EXPECT_EQ(Rational<s16>(std::pow(2.1, 18.0)), std::numeric_limits<s16>::max());
EXPECT_EQ(Rational<s16>(-std::pow(2.1, 18.0)), std::numeric_limits<s16>::min());
// Smallest positive non-zero float produces the smallest positive non-zero Rational.
EXPECT_EQ(Rational<s16>(std::numeric_limits<double>::denorm_min()),
Rational<s16>(1, std::numeric_limits<s16>::max()));
// Mixing types uses common_type
const auto big_result = s64(-1000) * Rational<u16>{3000, 3};
static_assert(std::is_same_v<decltype(big_result), const Rational<s64>>);
EXPECT_TRUE(big_result == -1000'000);
EXPECT_EQ(Rational<u32>(Rational(-6, -2)), 3);
EXPECT_EQ(Rational(5u) * Rational(-1), 0);
EXPECT_EQ(Rational(5u) * Rational(-1ll), -5);
// Works at compile time
static_assert(Rational(8, 2) + Rational(-6, 123) + 4 == Rational(326, 41));
}
// TODO: Add unit test coverage for `Rectangle::ClampUL`. (And consider removing
// `Rectangle::ClampLL`, which does not have any callers.)