mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
An SD865 profile of SotC (locked 60, Release, MTVU) shows __kernel_clock_gettime at 4.1% of the EE thread - the largest non-JIT symbol. GetCPUTicks() went through the vDSO (function call + seqlock + isb + ns conversion) on every read, and the framelimiter's frame-boundary spin in VMManager::Internal::Throttle() calls it tens of thousands of times per frame. Read the architected virtual counter directly instead: CNTVCT_EL0 ticks at CNTFRQ_EL0 (19.2MHz on SD865, 24MHz on Apple M2), is monotonic and consistent across cores, and is exactly what the vDSO reads underneath. Measured 1.24ns vs 13.39ns per call (10.8x) on M2/Asahi. GetTickFrequency() now returns CNTFRQ_EL0 so every freq-relative consumer is unaffected. Resolution drops from 1ns to ~52ns, ample for frame pacing and the sub-ms throttle spin. This mirrors a bare-mrs pattern used elsewhere for GS counters. Caller audit for the tick-unit change (only one thing assumed nanoseconds): - VMManager::Throttle, GSDumpReplayer, GSDevice present throttle, PerformanceMetrics, HostSys spin calibration: all convert through GetTickFrequency() - unaffected. (HostSys's "time > 100" calibration threshold now means 100 counter ticks, ~5.2us; the doubling loop just runs slightly longer.) - Threading::SleepUntil (Linux) built an absolute CLOCK_MONOTONIC timespec directly from the tick value, which breaks with raw counter ticks (different epoch). Rewritten as a relative sleep off the remaining delta with an early-wakeup retry loop, matching the Windows/Darwin implementations. - common/Perf.cpp jitdump timestamps use their own clock_gettime and must stay in perf's clock domain - untouched. - Common::Timer is a separate clock domain, never mixed with GetCPUTicks values - untouched. - GSRendererSW/GSFunctionMap LOG-gated dev stats print raw ticks - compile-time disabled dev code, freq-relative where it matters. x86 Linux and Darwin (already on mach_absolute_time) are unchanged. Validated: recompiler_tests 1083/1083 green; standalone check on M2/Asahi shows 0 monotonicity violations over 10M reads, 0.0002% error vs CLOCK_MONOTONIC over 500ms, SleepUntil overshoot ~60us (normal nanosleep wakeup latency) and instant return for past targets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
421 lines
12 KiB
C++
421 lines
12 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "common/Pcsx2Types.h"
|
|
#include "common/Console.h"
|
|
#include "common/HostSys.h"
|
|
#include "common/Path.h"
|
|
#include "common/ScopedGuard.h"
|
|
#include "common/SmallString.h"
|
|
#include "common/StringUtil.h"
|
|
#include "common/Threading.h"
|
|
#include "common/WindowInfo.h"
|
|
|
|
#include "fmt/format.h"
|
|
|
|
#include <dbus/dbus.h>
|
|
#include <spawn.h>
|
|
#include <sys/sysinfo.h>
|
|
#include <sys/time.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#if defined(X11_API)
|
|
#include <X11/Xlib.h>
|
|
#include <X11/extensions/XInput2.h>
|
|
#endif
|
|
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <ctype.h>
|
|
#include <optional>
|
|
#include <thread>
|
|
|
|
// Returns 0 on failure (not supported by the operating system).
|
|
u64 GetPhysicalMemory()
|
|
{
|
|
u64 pages = 0;
|
|
|
|
#ifdef _SC_PHYS_PAGES
|
|
pages = sysconf(_SC_PHYS_PAGES);
|
|
#endif
|
|
|
|
return pages * getpagesize();
|
|
}
|
|
|
|
u64 GetAvailablePhysicalMemory()
|
|
{
|
|
// Try to read MemAvailable from /proc/meminfo.
|
|
FILE* file = fopen("/proc/meminfo", "r");
|
|
if (file)
|
|
{
|
|
u64 mem_available = 0;
|
|
u64 mem_free = 0, buffers = 0, cached = 0, sreclaimable = 0, shmem = 0;
|
|
char line[256];
|
|
|
|
while (fgets(line, sizeof(line), file))
|
|
{
|
|
// Modern kernels provide MemAvailable directly - preferred and most accurate.
|
|
if (sscanf(line, "MemAvailable: %lu kB", &mem_available) == 1)
|
|
{
|
|
fclose(file);
|
|
return mem_available * _1kb;
|
|
}
|
|
|
|
// Fallback values for manual approximation.
|
|
sscanf(line, "MemFree: %lu kB", &mem_free);
|
|
sscanf(line, "Buffers: %lu kB", &buffers);
|
|
sscanf(line, "Cached: %lu kB", &cached);
|
|
sscanf(line, "SReclaimable: %lu kB", &sreclaimable);
|
|
sscanf(line, "Shmem: %lu kB", &shmem);
|
|
}
|
|
fclose(file);
|
|
|
|
// Fallback approximation: Linux-like heuristic.
|
|
// available = MemFree + Buffers + Cached + SReclaimable - Shmem.
|
|
const u64 available_kb = mem_free + buffers + cached + sreclaimable - shmem;
|
|
return available_kb * _1kb;
|
|
}
|
|
|
|
// Fallback to sysinfo if /proc/meminfo couldn't be read.
|
|
struct sysinfo info = {};
|
|
if (sysinfo(&info) != 0)
|
|
return 0;
|
|
|
|
// Note: This does NOT include cached memory - only free + buffer.
|
|
return (static_cast<u64>(info.freeram) + static_cast<u64>(info.bufferram)) * static_cast<u64>(info.mem_unit);
|
|
}
|
|
|
|
u64 GetTickFrequency()
|
|
{
|
|
#if defined(__aarch64__)
|
|
// Frequency of the architected virtual counter read by GetCPUTicks() (e.g. 19.2MHz on
|
|
// Snapdragon 865, 24MHz on Apple M2, 1GHz on ARMv8.6+ with FEAT_ECV).
|
|
u64 freq;
|
|
asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
|
|
return freq;
|
|
#else
|
|
return 1000000000; // unix measures in nanoseconds
|
|
#endif
|
|
}
|
|
|
|
u64 GetCPUTicks()
|
|
{
|
|
#if defined(__aarch64__)
|
|
// Read the architected virtual counter directly (a single mrs) rather than going through the
|
|
// vDSO clock_gettime() (function call + seqlock + isb + ns conversion) - this is the same
|
|
// counter the vDSO reads underneath. CNTVCT_EL0 is monotonic and consistent across cores, and
|
|
// Linux always enables EL0 counter access since its own vDSO fast path requires it. Resolution
|
|
// is coarser than 1ns (~52ns at 19.2MHz), which is ample for every consumer of this clock; all
|
|
// consumers must convert through GetTickFrequency() rather than assuming nanoseconds.
|
|
u64 val;
|
|
asm volatile("mrs %0, cntvct_el0" : "=r"(val)::"memory");
|
|
return val;
|
|
#else
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (static_cast<u64>(ts.tv_sec) * 1000000000ULL) + ts.tv_nsec;
|
|
#endif
|
|
}
|
|
|
|
std::string GetOSVersionString()
|
|
{
|
|
#if defined(__linux__)
|
|
FILE* file = fopen("/etc/os-release", "r");
|
|
if (file)
|
|
{
|
|
char line[256];
|
|
std::string distro;
|
|
std::string version = "";
|
|
while (fgets(line, sizeof(line), file))
|
|
{
|
|
std::string_view line_view(line);
|
|
if (line_view.starts_with("NAME="))
|
|
{
|
|
distro = line_view.substr(5, line_view.size() - 6);
|
|
}
|
|
else if (line_view.starts_with("BUILD_ID="))
|
|
{
|
|
version = line_view.substr(9, line_view.size() - 10);
|
|
}
|
|
else if (line_view.starts_with("VERSION_ID="))
|
|
{
|
|
version = line_view.substr(11, line_view.size() - 12);
|
|
}
|
|
}
|
|
fclose(file);
|
|
|
|
// Some distros put quotes around the name and or version.
|
|
if (distro.starts_with("\"") && distro.ends_with("\""))
|
|
distro = distro.substr(1, distro.size() - 2);
|
|
|
|
if (version.starts_with("\"") && version.ends_with("\""))
|
|
version = version.substr(1, version.size() - 2);
|
|
|
|
if (!distro.empty() && !version.empty())
|
|
return fmt::format("{} {}", distro, version);
|
|
}
|
|
|
|
return "Linux";
|
|
#else // freebsd
|
|
return "Other Unix";
|
|
#endif
|
|
}
|
|
|
|
static bool SetScreensaverInhibitDBus(const bool inhibit_requested, const char* program_name, const char* reason)
|
|
{
|
|
static dbus_uint32_t s_cookie;
|
|
const char* bus_method = (inhibit_requested) ? "Inhibit" : "UnInhibit";
|
|
DBusError error_dbus;
|
|
DBusConnection* connection = nullptr;
|
|
static DBusConnection* s_comparison_connection;
|
|
DBusMessage* message = nullptr;
|
|
DBusMessage* response = nullptr;
|
|
DBusMessageIter message_itr;
|
|
char* desktop_session = nullptr;
|
|
|
|
ScopedGuard cleanup = [&]() {
|
|
if (dbus_error_is_set(&error_dbus))
|
|
dbus_error_free(&error_dbus);
|
|
if (message)
|
|
dbus_message_unref(message);
|
|
if (response)
|
|
dbus_message_unref(response);
|
|
};
|
|
|
|
dbus_error_init(&error_dbus);
|
|
// Calling dbus_bus_get() after the first time returns a pointer to the existing connection.
|
|
connection = dbus_bus_get(DBUS_BUS_SESSION, &error_dbus);
|
|
if (!connection || (dbus_error_is_set(&error_dbus)))
|
|
return false;
|
|
if (s_comparison_connection != connection)
|
|
{
|
|
dbus_connection_set_exit_on_disconnect(connection, false);
|
|
s_cookie = 0;
|
|
s_comparison_connection = connection;
|
|
}
|
|
|
|
desktop_session = std::getenv("DESKTOP_SESSION");
|
|
if (desktop_session && std::strncmp(desktop_session, "mate", 4) == 0)
|
|
{
|
|
message = dbus_message_new_method_call("org.mate.ScreenSaver", "/org/mate/ScreenSaver", "org.mate.ScreenSaver", bus_method);
|
|
}
|
|
else
|
|
{
|
|
message = dbus_message_new_method_call("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", bus_method);
|
|
}
|
|
|
|
if (!message)
|
|
return false;
|
|
// Initialize an append iterator for the message, gets freed with the message.
|
|
dbus_message_iter_init_append(message, &message_itr);
|
|
if (inhibit_requested)
|
|
{
|
|
// Guard against repeat inhibitions which would add extra inhibitors each generating a different cookie.
|
|
if (s_cookie)
|
|
return false;
|
|
// Append process/window name.
|
|
if (!dbus_message_iter_append_basic(&message_itr, DBUS_TYPE_STRING, &program_name))
|
|
return false;
|
|
// Append reason for inhibiting the screensaver.
|
|
if (!dbus_message_iter_append_basic(&message_itr, DBUS_TYPE_STRING, &reason))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Only Append the cookie.
|
|
if (!dbus_message_iter_append_basic(&message_itr, DBUS_TYPE_UINT32, &s_cookie))
|
|
return false;
|
|
}
|
|
// Send message and get response.
|
|
response = dbus_connection_send_with_reply_and_block(connection, message, DBUS_TIMEOUT_USE_DEFAULT, &error_dbus);
|
|
if (!response || dbus_error_is_set(&error_dbus))
|
|
return false;
|
|
s_cookie = 0;
|
|
if (inhibit_requested)
|
|
{
|
|
// Get the cookie from the response message.
|
|
if (!dbus_message_get_args(response, &error_dbus, DBUS_TYPE_UINT32, &s_cookie, DBUS_TYPE_INVALID) || dbus_error_is_set(&error_dbus))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Common::InhibitScreensaver(bool inhibit)
|
|
{
|
|
return SetScreensaverInhibitDBus(inhibit, "PCSX2", "PCSX2 VM is running.");
|
|
}
|
|
|
|
#if defined(X11_API)
|
|
void Common::SetMousePosition(int x, int y)
|
|
{
|
|
Display* display = XOpenDisplay(nullptr);
|
|
if (!display)
|
|
return;
|
|
|
|
Window root = DefaultRootWindow(display);
|
|
XWarpPointer(display, None, root, 0, 0, 0, 0, x, y);
|
|
XFlush(display);
|
|
|
|
XCloseDisplay(display);
|
|
}
|
|
|
|
static std::function<void(int, int)> fnMouseMoveCb;
|
|
static std::atomic<bool> trackingMouse = false;
|
|
static std::thread mouseThread;
|
|
|
|
void mouseEventLoop()
|
|
{
|
|
Threading::SetNameOfCurrentThread("X11 Mouse Thread");
|
|
Display* display = XOpenDisplay(nullptr);
|
|
if (!display)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int opcode, eventcode, error;
|
|
if (!XQueryExtension(display, "XInputExtension", &opcode, &eventcode, &error))
|
|
{
|
|
XCloseDisplay(display);
|
|
return;
|
|
}
|
|
|
|
const Window root = DefaultRootWindow(display);
|
|
XIEventMask evmask;
|
|
unsigned char mask[(XI_LASTEVENT + 7) / 8] = {0};
|
|
|
|
evmask.deviceid = XIAllDevices;
|
|
evmask.mask_len = sizeof(mask);
|
|
evmask.mask = mask;
|
|
XISetMask(mask, XI_RawMotion);
|
|
|
|
XISelectEvents(display, root, &evmask, 1);
|
|
XSync(display, False);
|
|
|
|
XEvent event;
|
|
while (trackingMouse)
|
|
{
|
|
// XNextEvent is blocking, this is a zombie process risk if no events arrive
|
|
// while we are trying to shutdown.
|
|
// https://nrk.neocities.org/articles/x11-timeout-with-xsyncalarm might be
|
|
// a better solution than using XPending.
|
|
if (!XPending(display))
|
|
{
|
|
Threading::Sleep(1);
|
|
Threading::SpinWait();
|
|
continue;
|
|
}
|
|
|
|
XNextEvent(display, &event);
|
|
if (event.xcookie.type == GenericEvent &&
|
|
event.xcookie.extension == opcode &&
|
|
XGetEventData(display, &event.xcookie))
|
|
{
|
|
XIRawEvent* raw_event = reinterpret_cast<XIRawEvent*>(event.xcookie.data);
|
|
if (raw_event->evtype == XI_RawMotion)
|
|
{
|
|
Window w;
|
|
int root_x, root_y, win_x, win_y;
|
|
unsigned int mask;
|
|
XQueryPointer(display, root, &w, &w, &root_x, &root_y, &win_x, &win_y, &mask);
|
|
|
|
if (fnMouseMoveCb)
|
|
fnMouseMoveCb(root_x, root_y);
|
|
}
|
|
XFreeEventData(display, &event.xcookie);
|
|
}
|
|
}
|
|
|
|
XCloseDisplay(display);
|
|
}
|
|
|
|
bool Common::AttachMousePositionCb(std::function<void(int, int)> cb)
|
|
{
|
|
fnMouseMoveCb = cb;
|
|
|
|
if (trackingMouse)
|
|
return true;
|
|
|
|
trackingMouse = true;
|
|
mouseThread = std::thread(mouseEventLoop);
|
|
mouseThread.detach();
|
|
return true;
|
|
}
|
|
|
|
void Common::DetachMousePositionCb()
|
|
{
|
|
trackingMouse = false;
|
|
fnMouseMoveCb = nullptr;
|
|
if (mouseThread.joinable())
|
|
{
|
|
mouseThread.join();
|
|
}
|
|
}
|
|
#endif // X11_API
|
|
|
|
bool Common::PlaySoundAsync(const char* path)
|
|
{
|
|
#ifdef __linux__
|
|
// This is... pretty awful. But I can't think of a better way without linking to e.g. gstreamer.
|
|
const char* cmdname = "aplay";
|
|
const char* argv[] = {cmdname, path, nullptr};
|
|
pid_t pid;
|
|
|
|
// Since we set SA_NOCLDWAIT in Qt, we don't need to wait here.
|
|
int res = posix_spawnp(&pid, cmdname, nullptr, nullptr, const_cast<char**>(argv), environ);
|
|
if (res == 0)
|
|
return true;
|
|
|
|
// Try gst-play-1.0.
|
|
const char* gst_play_cmdname = "gst-play-1.0";
|
|
const char* gst_play_argv[] = {cmdname, path, nullptr};
|
|
res = posix_spawnp(&pid, gst_play_cmdname, nullptr, nullptr, const_cast<char**>(gst_play_argv), environ);
|
|
if (res == 0)
|
|
return true;
|
|
|
|
// gst-launch? Bit messier for sure.
|
|
TinyString location_str = TinyString::from_format("location={}", path);
|
|
TinyString parse_str = TinyString::from_format("{}parse", Path::GetExtension(path));
|
|
const char* gst_launch_cmdname = "gst-launch-1.0";
|
|
const char* gst_launch_argv[] = {
|
|
gst_launch_cmdname, "filesrc", location_str.c_str(), "!", parse_str.c_str(), "!", "alsasink", nullptr};
|
|
res = posix_spawnp(&pid, gst_launch_cmdname, nullptr, nullptr, const_cast<char**>(gst_launch_argv), environ);
|
|
if (res == 0)
|
|
return true;
|
|
|
|
Console.ErrorFmt("Failed to play sound effect {}. Make sure you have aplay, gst-play-1.0, or gst-launch-1.0 available.", path);
|
|
return false;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void Threading::Sleep(int ms)
|
|
{
|
|
usleep(1000 * ms);
|
|
}
|
|
|
|
void Threading::SleepUntil(u64 ticks)
|
|
{
|
|
// GetCPUTicks() values are not necessarily in the CLOCK_MONOTONIC domain (on AArch64 they are
|
|
// raw CNTVCT_EL0 counter values with a different epoch), so we can't use TIMER_ABSTIME here.
|
|
// Sleep off the remaining delta instead, retrying on early wakeups, matching the Windows and
|
|
// Darwin implementations.
|
|
for (;;)
|
|
{
|
|
const s64 diff = static_cast<s64>(ticks - GetCPUTicks());
|
|
if (diff <= 0)
|
|
return;
|
|
|
|
const u64 freq = GetTickFrequency();
|
|
struct timespec ts;
|
|
ts.tv_sec = static_cast<time_t>(static_cast<u64>(diff) / freq);
|
|
ts.tv_nsec = static_cast<long>(((static_cast<u64>(diff) % freq) * 1000000000ULL) / freq);
|
|
|
|
const int err = clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, nullptr);
|
|
if (err != 0 && err != EINTR)
|
|
return;
|
|
}
|
|
}
|