aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-30 01:55:20 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-30 02:31:58 -0400
commit0218eacfd560d0ce48b3ffe64d279ee85bbf1347 (patch)
treecbe764aa642b55cae06e94f15bd0e16f234e6052 /src
parent08be623a271d13b66497a98265f6832e9aa2c0d0 (diff)
nx2: run the guest counter at 31.25 MHz for Switch 2 titles
The Switch 2 counter runs at 31.25 MHz and its SDK converts ticks with that constant: Mario Kart World's sdk builds 31,250,000 in nn::os::GetSystemTickFrequency, TickManager::ConvertToTimeSpan and TickManager::ConvertToTick (19.2 MHz appears nowhere). Eglantine counted at the Switch 1 rate for every title, so every tick-based time in a Switch 2 game was off by 1.63x: elapsed times read short, and a deadline of one 60 Hz frame converted to ticks waited ~27 ms. The wall clock now has a guest counter frequency, set at boot: 31.25 MHz for Switch 2 programs, 19.2 MHz otherwise, EDEN_NX2_CNTFRQ=<Hz> to force one. CNTPCT reads, CNTFRQ_EL0, svcGetSystemTick, thread tick counts and the time services' tick conversions follow it, and the CNTPCT spin detection keeps its ~1.7 us threshold. Mario Kart World, title screen and attract demo, 180 s, same build: frame times unchanged (17-19 ms once warm), process CPU time 848 s instead of 930 s.
Diffstat (limited to 'src')
-rw-r--r--src/common/cpu_features.cpp31
-rw-r--r--src/common/cpu_features.h22
-rw-r--r--src/core/arm/dynarmic/arm_dynarmic_64.cpp6
-rw-r--r--src/core/arm/dynarmic/arm_dynarmic_64.h1
-rw-r--r--src/core/arm/nce/patcher.cpp3
-rw-r--r--src/core/core.cpp25
-rw-r--r--src/core/core_timing.cpp2
-rw-r--r--src/core/hle/service/psc/time/common.h11
8 files changed, 81 insertions, 20 deletions
diff --git a/src/common/cpu_features.cpp b/src/common/cpu_features.cpp
index 52fd003c44..93eecb1dc5 100644
--- a/src/common/cpu_features.cpp
+++ b/src/common/cpu_features.cpp
@@ -278,8 +278,16 @@ std::chrono::milliseconds WallClock::GetTimeMS() const {
s64 WallClock::GetCNTPCT() const {
if (!invariant)
- return GetUptime() * NsToCNTPCTRatio::num / NsToCNTPCTRatio::den;
- return MultiplyHigh(GetUptime(), cntpct_rdtsc_factor);
+ return MultiplyAndDivide64(GetUptime(), GetGuestCNTFRQ(), NsRatio::den);
+ return MultiplyHigh(GetUptime(), cntpct_rdtsc_factor.load(std::memory_order_relaxed));
+}
+
+void WallClock::SetGuestCNTFRQ(u64 frequency) {
+ guest_cntfrq.store(frequency, std::memory_order_relaxed);
+ if (invariant) {
+ cntpct_rdtsc_factor.store(GetFixedPoint64Factor(frequency, rdtsc_frequency),
+ std::memory_order_relaxed);
+ }
}
s64 WallClock::GetGPUTick() const {
@@ -336,7 +344,7 @@ namespace {
} // namespace
WallClock::WallClock(bool invariant_, u64 rdtsc_frequency_) noexcept {
- const u64 host_cntfrq = std::max<u64>(GetHostCNTFRQ(), 1);
+ host_cntfrq = std::max<u64>(GetHostCNTFRQ(), 1);
ns_cntfrq_factor = GetFixedPointFactor(NsRatio::den, host_cntfrq);
us_cntfrq_factor = GetFixedPointFactor(UsRatio::den, host_cntfrq);
ms_cntfrq_factor = GetFixedPointFactor(MsRatio::den, host_cntfrq);
@@ -361,6 +369,11 @@ s64 WallClock::GetCNTPCT() const {
return MultiplyHigh(GetUptime(), guest_cntfrq_factor);
}
+void WallClock::SetGuestCNTFRQ(u64 frequency) {
+ guest_cntfrq.store(frequency, std::memory_order_relaxed);
+ guest_cntfrq_factor = GetFixedPointFactor(frequency, host_cntfrq);
+}
+
s64 WallClock::GetGPUTick() const {
return MultiplyHigh(GetUptime(), gputick_cntfrq_factor);
}
@@ -399,7 +412,11 @@ std::chrono::milliseconds WallClock::GetTimeMS() const {
}
s64 WallClock::GetCNTPCT() const {
- return GetUptime() * NsToCNTPCTRatio::num / NsToCNTPCTRatio::den;
+ return MultiplyAndDivide64(GetUptime(), GetGuestCNTFRQ(), NsRatio::den);
+}
+
+void WallClock::SetGuestCNTFRQ(u64 frequency) {
+ guest_cntfrq.store(frequency, std::memory_order_relaxed);
}
s64 WallClock::GetGPUTick() const {
@@ -419,9 +436,13 @@ u64 WallClock::NsToTicks(std::chrono::nanoseconds ns) const {
}
#endif
+u64 WallClock::CPUTickToGuestCNTPCT(u64 cpu_tick) const {
+ return MultiplyAndDivide64(cpu_tick, GetGuestCNTFRQ(), CPUTickFreq);
+}
+
// Wall clock MUST be initialized AFTER g_cpu_caps
// C++ only guarantees ctor init in the order they appear in TU
-const WallClock g_wall_clock = [] {
+WallClock g_wall_clock = [] {
#if defined(ARCHITECTURE_x86_64)
auto const& caps = Common::g_cpu_caps;
return WallClock(caps.invariant_tsc && caps.tsc_frequency > std::nano::den, caps.tsc_frequency);
diff --git a/src/common/cpu_features.h b/src/common/cpu_features.h
index 61aa2767ab..31be2dc956 100644
--- a/src/common/cpu_features.h
+++ b/src/common/cpu_features.h
@@ -7,6 +7,7 @@
#pragma once
+#include <atomic>
#include <optional>
#include <string_view>
#include <chrono>
@@ -20,11 +21,24 @@ namespace Common {
class WallClock {
public:
static constexpr u64 CNTFRQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
+ static constexpr u64 Switch2CNTFRQ = 31'250'000; // Switch 2 CNTPCT_EL0 Frequency = 31.25 MHz
static constexpr u64 GPUTickFreq = 614'400'000; // GM20B GPU Tick Frequency = 614.4 MHz
static constexpr u64 CPUTickFreq = 1'020'000'000; // T210/4 A57 CPU Tick Frequency = 1020.0 MHz
explicit WallClock(bool invariant, u64 rdtsc_frequency_) noexcept;
+ /// Sets the guest counter's frequency: CNTFRQ, or Switch2CNTFRQ for Switch 2 titles (their SDK
+ /// converts ticks with a constant 31.25 MHz). Called at boot, before guest code runs.
+ void SetGuestCNTFRQ(u64 frequency);
+
+ /// @returns The guest counter's frequency (CNTFRQ_EL0).
+ [[nodiscard]] u64 GetGuestCNTFRQ() const {
+ return guest_cntfrq.load(std::memory_order_relaxed);
+ }
+
+ /// @returns Emulated CPU ticks (single core timing) as guest CNTPCT ticks.
+ [[nodiscard]] u64 CPUTickToGuestCNTPCT(u64 cpu_tick) const;
+
/// @returns The time in nanoseconds since the construction of this clock.
std::chrono::nanoseconds GetTimeNS() const;
@@ -34,7 +48,7 @@ public:
/// @returns The time in milliseconds since the construction of this clock.
std::chrono::milliseconds GetTimeMS() const;
- /// @returns The guest CNTPCT ticks since the construction of this clock.
+ /// @returns The guest CNTPCT ticks (at GetGuestCNTFRQ()) since the construction of this clock.
s64 GetCNTPCT() const;
/// @returns The guest GPU ticks since the construction of this clock.
@@ -91,6 +105,7 @@ public:
using CPUTickToCNTPCTRatio = std::ratio<CNTFRQ, CPUTickFreq>;
using CPUTickToGPUTickRatio = std::ratio<GPUTickFreq, CPUTickFreq>;
+ std::atomic<u64> guest_cntfrq{CNTFRQ};
#if defined(ARCHITECTURE_x86_64)
u64 rdtsc_frequency;
u64 ns_rdtsc_factor;
@@ -98,7 +113,7 @@ public:
u64 ms_rdtsc_factor;
u64 rdtsc_ns_integer;
u64 rdtsc_ns_factor;
- u64 cntpct_rdtsc_factor;
+ std::atomic<u64> cntpct_rdtsc_factor;
u64 gputick_rdtsc_factor;
bool invariant;
#elif defined(HAS_NCE)
@@ -106,6 +121,7 @@ public:
[[nodiscard]] inline FactorType GetGuestCNTFRQFactor() const {
return guest_cntfrq_factor;
}
+ u64 host_cntfrq;
FactorType ns_cntfrq_factor;
FactorType us_cntfrq_factor;
FactorType ms_cntfrq_factor;
@@ -183,6 +199,6 @@ std::optional<int> GetProcessorCount();
/// @brief Global cpu caps
extern const CPUCaps g_cpu_caps;
/// @brief Global wall clock
-extern const WallClock g_wall_clock;
+extern WallClock g_wall_clock;
} // namespace Common
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.cpp b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
index a2c0124f33..e65e38dd9e 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
@@ -180,7 +180,7 @@ u64 DynarmicCallbacks64::GetCNTPCT() {
const char* value = std::getenv("EDEN_NX2_CNTPCT_PAUSE");
return value == nullptr || value[0] != '0';
}();
- if (pause_spins && ticks - m_last_cntpct < 32) {
+ if (pause_spins && ticks - m_last_cntpct < m_spin_ticks) {
#if defined(_M_X64) || defined(__x86_64__)
_mm_pause();
_mm_pause();
@@ -269,7 +269,9 @@ void ArmDynarmic64::MakeJit(Common::PageTable* page_table, std::size_t address_s
config.tpidr_el0 = &m_cb->m_tpidr_el0;
config.dczid_el0 = 4;
config.ctr_el0 = 0x8444c004;
- config.cntfrq_el0 = Hardware::CNTFREQ;
+ // 31.25 MHz for Switch 2 titles (Common::WallClock::SetGuestCNTFRQ, set at boot).
+ config.cntfrq_el0 = static_cast<u32>(Common::g_wall_clock.GetGuestCNTFRQ());
+ m_cb->m_spin_ticks = config.cntfrq_el0 / 600'000;
// Unpredictable instructions
config.define_unpredictable_behaviour = true;
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.h b/src/core/arm/dynarmic/arm_dynarmic_64.h
index acd1fe773f..66b07474c1 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.h
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.h
@@ -66,6 +66,7 @@ public:
u64 m_tpidrro_el0{};
u64 m_tpidr_el0{};
u64 m_last_cntpct{}; ///< The previous counter read (GetCNTPCT spin detection)
+ u64 m_spin_ticks{32}; ///< Reads closer than this (~1.7 us) count as a polling loop
Kernel::KProcess* m_process{};
const bool m_debugger_enabled{};
const bool m_check_memory_access{};
diff --git a/src/core/arm/nce/patcher.cpp b/src/core/arm/nce/patcher.cpp
index f21d7cacdc..72c3497f03 100644
--- a/src/core/arm/nce/patcher.cpp
+++ b/src/core/arm/nce/patcher.cpp
@@ -627,7 +627,7 @@ void Patcher::WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg,
}
void Patcher::WriteCntfrqHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg, oaknut::VectorCodeGenerator& cg) {
- cg.MOV(dest_reg, Common::WallClock::CNTFRQ);
+ cg.MOV(dest_reg, Common::g_wall_clock.GetGuestCNTFRQ());
// Jump back to the instruction after the emulated MRS.
if (&cg == &c_pre)
@@ -642,6 +642,7 @@ void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_
#else
static Common::WallClock clock(true, 1);
#endif
+ clock.SetGuestCNTFRQ(Common::g_wall_clock.GetGuestCNTFRQ());
const auto factor = clock.GetGuestCNTFRQFactor();
const auto raw_factor = std::bit_cast<std::array<u64, 2>>(factor);
diff --git a/src/core/core.cpp b/src/core/core.cpp
index f3d224b1ce..3728ccf8f7 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -2,11 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <array>
+#include <cstdlib>
#include <atomic>
#include <memory>
#include <utility>
#include "game_settings.h"
+#include "common/cpu_features.h"
#include "audio_core/audio_core.h"
#include "common/fs/fs.h"
#include "common/logging.h"
@@ -313,16 +315,33 @@ struct System::Impl {
fs_controller.InitTempStorage();
}
+ const bool switch2_program =
+ virtual_filesystem != nullptr &&
+ IsSwitch2Program(GetGameFileFromPath(virtual_filesystem, filepath));
+
// Switch 2 titles size their heap from the total memory and need the 12 GiB layout. Set it
// as a per-game value, so RestoreGlobalState brings back the configured layout afterwards.
- if (virtual_filesystem != nullptr &&
- Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_12Gb &&
- IsSwitch2Program(GetGameFileFromPath(virtual_filesystem, filepath))) {
+ if (switch2_program &&
+ Settings::values.memory_layout_mode.GetValue() != Settings::MemoryLayout::Memory_12Gb) {
LOG_INFO(Core, "Switch 2 program: using the 12 GiB memory layout");
Settings::values.memory_layout_mode.SetGlobal(false);
Settings::values.memory_layout_mode.SetValue(Settings::MemoryLayout::Memory_12Gb);
}
+ // The Switch 2 counter runs at 31.25 MHz and its SDK converts ticks with that constant
+ // (nn::os::GetSystemTickFrequency, TickManager::ConvertToTimeSpan/ConvertToTick). At 19.2 MHz
+ // every tick-based time in the game was off by 1.63x: elapsed times read short and a frame
+ // deadline in ticks waited ~27 ms. EDEN_NX2_CNTFRQ=<Hz> overrides it (A/B tests).
+ static const u64 forced_cntfrq = [] {
+ const char* value = std::getenv("EDEN_NX2_CNTFRQ");
+ return value != nullptr ? std::strtoull(value, nullptr, 10) : 0ULL;
+ }();
+ const u64 cntfrq = forced_cntfrq != 0 ? forced_cntfrq
+ : switch2_program ? Common::WallClock::Switch2CNTFRQ
+ : Common::WallClock::CNTFRQ;
+ Common::g_wall_clock.SetGuestCNTFRQ(cntfrq);
+ LOG_INFO(Core, "Guest counter frequency: {} Hz", cntfrq);
+
InitializeKernel(system);
if (params.applet_type == Service::AM::AppletType::Application) {
diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp
index b85bd30a58..dfc3f6f2a6 100644
--- a/src/core/core_timing.cpp
+++ b/src/core/core_timing.cpp
@@ -224,7 +224,7 @@ void CoreTiming::ResetTicks() {
}
u64 CoreTiming::GetClockTicks() const {
- u64 fres = is_multicore ? Common::g_wall_clock.GetCNTPCT() : Common::WallClock::CPUTickToCNTPCT(cpu_ticks);
+ u64 fres = is_multicore ? Common::g_wall_clock.GetCNTPCT() : Common::g_wall_clock.CPUTickToGuestCNTPCT(cpu_ticks);
if (const u32 target = CpuClockTargetMhz(Settings::values.cpu_clock.GetValue());
target != CPU_CLOCK_BASE_MHZ) {
fres = fres * target / CPU_CLOCK_BASE_MHZ;
diff --git a/src/core/hle/service/psc/time/common.h b/src/core/hle/service/psc/time/common.h
index 2183fa72b8..6f7d2cb88b 100644
--- a/src/core/hle/service/psc/time/common.h
+++ b/src/core/hle/service/psc/time/common.h
@@ -134,12 +134,13 @@ struct OperationEvent : public Common::IntrusiveListBaseNode<OperationEvent> {
Kernel::KEvent* m_event{};
};
-constexpr inline std::chrono::nanoseconds ConvertToTimeSpan(s64 ticks) {
+inline std::chrono::nanoseconds ConvertToTimeSpan(s64 ticks) {
constexpr auto one_second_ns{
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::seconds(1)).count()};
- constexpr s64 max{Common::WallClock::CNTFRQ *
- ((std::numeric_limits<s64>::max)() / one_second_ns)};
+ // The guest counter's frequency: 31.25 MHz for Switch 2 titles.
+ const s64 frequency{static_cast<s64>(Common::g_wall_clock.GetGuestCNTFRQ())};
+ const s64 max{frequency * ((std::numeric_limits<s64>::max)() / one_second_ns)};
if (ticks > max) {
return std::chrono::nanoseconds((std::numeric_limits<s64>::max)());
@@ -147,8 +148,8 @@ constexpr inline std::chrono::nanoseconds ConvertToTimeSpan(s64 ticks) {
return std::chrono::nanoseconds((std::numeric_limits<s64>::min)());
}
- auto a{ticks / Common::WallClock::CNTFRQ * one_second_ns};
- auto b{((ticks % Common::WallClock::CNTFRQ) * one_second_ns) / Common::WallClock::CNTFRQ};
+ auto a{ticks / frequency * one_second_ns};
+ auto b{((ticks % frequency) * one_second_ns) / frequency};
return std::chrono::nanoseconds(a + b);
}