First Commit
This commit is contained in:
372
common/Linux/LnxHostSys.cpp
Normal file
372
common/Linux/LnxHostSys.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "common/Assertions.h"
|
||||
#include "common/BitUtils.h"
|
||||
#include "common/Console.h"
|
||||
#include "common/CrashHandler.h"
|
||||
#include "common/Error.h"
|
||||
#include "common/HostSys.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <csignal>
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <mutex>
|
||||
#include <sys/mman.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "fmt/format.h"
|
||||
|
||||
#if defined(__FreeBSD__)
|
||||
#include "cpuinfo.h"
|
||||
#endif
|
||||
|
||||
// FreeBSD does not have MAP_FIXED_NOREPLACE, but does have MAP_EXCL.
|
||||
// MAP_FIXED combined with MAP_EXCL behaves like MAP_FIXED_NOREPLACE.
|
||||
#if defined(__FreeBSD__) && !defined(MAP_FIXED_NOREPLACE)
|
||||
#define MAP_FIXED_NOREPLACE (MAP_FIXED | MAP_EXCL)
|
||||
#endif
|
||||
|
||||
static __ri uint LinuxProt(const PageProtectionMode& mode)
|
||||
{
|
||||
u32 lnxmode = 0;
|
||||
|
||||
if (mode.CanWrite())
|
||||
lnxmode |= PROT_WRITE;
|
||||
if (mode.CanRead())
|
||||
lnxmode |= PROT_READ;
|
||||
if (mode.CanExecute())
|
||||
lnxmode |= PROT_EXEC | PROT_READ;
|
||||
|
||||
return lnxmode;
|
||||
}
|
||||
|
||||
void* HostSys::Mmap(void* base, size_t size, const PageProtectionMode& mode)
|
||||
{
|
||||
pxAssertMsg((size & (__pagesize - 1)) == 0, "Size is page aligned");
|
||||
|
||||
if (mode.IsNone())
|
||||
return nullptr;
|
||||
|
||||
const u32 prot = LinuxProt(mode);
|
||||
|
||||
u32 flags = MAP_PRIVATE | MAP_ANONYMOUS;
|
||||
if (base)
|
||||
flags |= MAP_FIXED_NOREPLACE;
|
||||
|
||||
void* res = mmap(base, size, prot, flags, -1, 0);
|
||||
if (res == MAP_FAILED)
|
||||
return nullptr;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void HostSys::Munmap(void* base, size_t size)
|
||||
{
|
||||
if (!base)
|
||||
return;
|
||||
|
||||
munmap((void*)base, size);
|
||||
}
|
||||
|
||||
void HostSys::MemProtect(void* baseaddr, size_t size, const PageProtectionMode& mode)
|
||||
{
|
||||
pxAssertMsg((size & (__pagesize - 1)) == 0, "Size is page aligned");
|
||||
|
||||
const u32 lnxmode = LinuxProt(mode);
|
||||
|
||||
const int result = mprotect(baseaddr, size, lnxmode);
|
||||
if (result != 0)
|
||||
pxFail("mprotect() failed");
|
||||
}
|
||||
|
||||
std::string HostSys::GetFileMappingName(const char* prefix)
|
||||
{
|
||||
const unsigned pid = static_cast<unsigned>(getpid());
|
||||
#if defined(__FreeBSD__)
|
||||
// FreeBSD's shm_open(3) requires name to be absolute
|
||||
return fmt::format("/tmp/{}_{}", prefix, pid);
|
||||
#else
|
||||
return fmt::format("{}_{}", prefix, pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
void* HostSys::CreateSharedMemory(const char* name, size_t size)
|
||||
{
|
||||
const int fd = shm_open(name, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
if (fd < 0)
|
||||
{
|
||||
std::fprintf(stderr, "shm_open failed: %d\n", errno);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// we're not going to be opening this mapping in other processes, so remove the file
|
||||
shm_unlink(name);
|
||||
|
||||
// ensure it's the correct size
|
||||
if (ftruncate(fd, static_cast<off_t>(size)) < 0)
|
||||
{
|
||||
std::fprintf(stderr, "ftruncate(%zu) failed: %d\n", size, errno);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return reinterpret_cast<void*>(static_cast<intptr_t>(fd));
|
||||
}
|
||||
|
||||
void HostSys::DestroySharedMemory(void* ptr)
|
||||
{
|
||||
close(static_cast<int>(reinterpret_cast<intptr_t>(ptr)));
|
||||
}
|
||||
|
||||
void* HostSys::MapSharedMemory(void* handle, size_t offset, void* baseaddr, size_t size, const PageProtectionMode& mode)
|
||||
{
|
||||
const uint lnxmode = LinuxProt(mode);
|
||||
|
||||
const int flags = (baseaddr != nullptr) ? (MAP_SHARED | MAP_FIXED_NOREPLACE) : MAP_SHARED;
|
||||
void* ptr = mmap(baseaddr, size, lnxmode, flags, static_cast<int>(reinterpret_cast<intptr_t>(handle)), static_cast<off_t>(offset));
|
||||
if (ptr == MAP_FAILED)
|
||||
return nullptr;
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void HostSys::UnmapSharedMemory(void* baseaddr, size_t size)
|
||||
{
|
||||
if (munmap(baseaddr, size) != 0)
|
||||
pxFailRel("Failed to unmap shared memory");
|
||||
}
|
||||
|
||||
size_t HostSys::GetRuntimePageSize()
|
||||
{
|
||||
int res = sysconf(_SC_PAGESIZE);
|
||||
return (res > 0) ? static_cast<size_t>(res) : 0;
|
||||
}
|
||||
|
||||
size_t HostSys::GetRuntimeCacheLineSize()
|
||||
{
|
||||
#if defined(__FreeBSD__)
|
||||
if (!cpuinfo_initialize())
|
||||
return 0;
|
||||
|
||||
u32 max_line_size = 0;
|
||||
for (u32 i = 0; i < cpuinfo_get_processors_count(); i++)
|
||||
{
|
||||
const u32 l1i = cpuinfo_get_processor(i)->cache.l1i->line_size;
|
||||
const u32 l1d = cpuinfo_get_processor(i)->cache.l1d->line_size;
|
||||
const u32 res = std::max<u32>(l1i, l1d);
|
||||
|
||||
max_line_size = std::max<u32>(max_line_size, res);
|
||||
}
|
||||
|
||||
return static_cast<size_t>(max_line_size);
|
||||
#else
|
||||
int l1i = sysconf(_SC_LEVEL1_DCACHE_LINESIZE);
|
||||
int l1d = sysconf(_SC_LEVEL1_ICACHE_LINESIZE);
|
||||
int res = (l1i > l1d) ? l1i : l1d;
|
||||
for (int index = 0; index < 16; index++)
|
||||
{
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "/sys/devices/system/cpu/cpu0/cache/index%d/coherency_line_size", index);
|
||||
std::FILE* fp = std::fopen(buf, "rb");
|
||||
if (!fp)
|
||||
break;
|
||||
|
||||
std::fread(buf, sizeof(buf), 1, fp);
|
||||
std::fclose(fp);
|
||||
int val = std::atoi(buf);
|
||||
res = (val > res) ? val : res;
|
||||
}
|
||||
|
||||
return (res > 0) ? static_cast<size_t>(res) : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
SharedMemoryMappingArea::SharedMemoryMappingArea(u8* base_ptr, size_t size, size_t num_pages)
|
||||
: m_base_ptr(base_ptr)
|
||||
, m_size(size)
|
||||
, m_num_pages(num_pages)
|
||||
{
|
||||
}
|
||||
|
||||
SharedMemoryMappingArea::~SharedMemoryMappingArea()
|
||||
{
|
||||
pxAssertRel(m_num_mappings == 0, "No mappings left");
|
||||
|
||||
if (munmap(m_base_ptr, m_size) != 0)
|
||||
pxFailRel("Failed to release shared memory area");
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<SharedMemoryMappingArea> SharedMemoryMappingArea::Create(size_t size)
|
||||
{
|
||||
pxAssertRel(Common::IsAlignedPow2(size, __pagesize), "Size is page aligned");
|
||||
|
||||
void* alloc = mmap(nullptr, size, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
|
||||
if (alloc == MAP_FAILED)
|
||||
return nullptr;
|
||||
|
||||
return std::unique_ptr<SharedMemoryMappingArea>(new SharedMemoryMappingArea(static_cast<u8*>(alloc), size, size / __pagesize));
|
||||
}
|
||||
|
||||
u8* SharedMemoryMappingArea::Map(void* file_handle, size_t file_offset, void* map_base, size_t map_size, const PageProtectionMode& mode)
|
||||
{
|
||||
pxAssert(static_cast<u8*>(map_base) >= m_base_ptr && static_cast<u8*>(map_base) < (m_base_ptr + m_size));
|
||||
|
||||
// MAP_FIXED is okay here, since we've reserved the entire region, and *want* to overwrite the mapping.
|
||||
const uint lnxmode = LinuxProt(mode);
|
||||
void* const ptr = mmap(map_base, map_size, lnxmode, MAP_SHARED | MAP_FIXED,
|
||||
static_cast<int>(reinterpret_cast<intptr_t>(file_handle)), static_cast<off_t>(file_offset));
|
||||
if (ptr == MAP_FAILED)
|
||||
return nullptr;
|
||||
|
||||
m_num_mappings++;
|
||||
return static_cast<u8*>(ptr);
|
||||
}
|
||||
|
||||
bool SharedMemoryMappingArea::Unmap(void* map_base, size_t map_size)
|
||||
{
|
||||
pxAssert(static_cast<u8*>(map_base) >= m_base_ptr && static_cast<u8*>(map_base) < (m_base_ptr + m_size));
|
||||
|
||||
if (mmap(map_base, map_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED)
|
||||
return false;
|
||||
|
||||
m_num_mappings--;
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace PageFaultHandler
|
||||
{
|
||||
static std::recursive_mutex s_exception_handler_mutex;
|
||||
static bool s_in_exception_handler = false;
|
||||
static bool s_installed = false;
|
||||
} // namespace PageFaultHandler
|
||||
|
||||
#ifdef _M_ARM64
|
||||
|
||||
void HostSys::FlushInstructionCache(void* address, u32 size)
|
||||
{
|
||||
__builtin___clear_cache(reinterpret_cast<char*>(address), reinterpret_cast<char*>(address) + size);
|
||||
}
|
||||
|
||||
[[maybe_unused]] static bool IsStoreInstruction(const void* ptr)
|
||||
{
|
||||
u32 bits;
|
||||
std::memcpy(&bits, ptr, sizeof(bits));
|
||||
|
||||
// Based on vixl's disassembler Instruction::IsStore().
|
||||
// if (Mask(LoadStoreAnyFMask) != LoadStoreAnyFixed)
|
||||
if ((bits & 0x0a000000) != 0x08000000)
|
||||
return false;
|
||||
|
||||
// if (Mask(LoadStorePairAnyFMask) == LoadStorePairAnyFixed)
|
||||
if ((bits & 0x3a000000) == 0x28000000)
|
||||
{
|
||||
// return Mask(LoadStorePairLBit) == 0
|
||||
return (bits & (1 << 22)) == 0;
|
||||
}
|
||||
|
||||
switch (bits & 0xC4C00000)
|
||||
{
|
||||
case 0x00000000: // STRB_w
|
||||
case 0x40000000: // STRH_w
|
||||
case 0x80000000: // STR_w
|
||||
case 0xC0000000: // STR_x
|
||||
case 0x04000000: // STR_b
|
||||
case 0x44000000: // STR_h
|
||||
case 0x84000000: // STR_s
|
||||
case 0xC4000000: // STR_d
|
||||
case 0x04800000: // STR_q
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // _M_ARM64
|
||||
|
||||
namespace PageFaultHandler
|
||||
{
|
||||
static void SignalHandler(int sig, siginfo_t* info, void* ctx);
|
||||
} // namespace PageFaultHandler
|
||||
|
||||
void PageFaultHandler::SignalHandler(int sig, siginfo_t* info, void* ctx)
|
||||
{
|
||||
#if defined(__linux__)
|
||||
void* const exception_address = reinterpret_cast<void*>(info->si_addr);
|
||||
|
||||
#if defined(_M_X86)
|
||||
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.gregs[REG_RIP]);
|
||||
const bool is_write = (static_cast<ucontext_t*>(ctx)->uc_mcontext.gregs[REG_ERR] & 2) != 0;
|
||||
#elif defined(_M_ARM64)
|
||||
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.pc);
|
||||
const bool is_write = IsStoreInstruction(exception_pc);
|
||||
#endif
|
||||
|
||||
#elif defined(__FreeBSD__)
|
||||
|
||||
#if defined(_M_X86)
|
||||
void* const exception_address = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_addr);
|
||||
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_rip);
|
||||
const bool is_write = (static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_err & 2) != 0;
|
||||
#elif defined(_M_ARM64)
|
||||
void* const exception_address = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__es.__far);
|
||||
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__ss.__pc);
|
||||
const bool is_write = IsStoreInstruction(exception_pc);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
// Executing the handler concurrently from multiple threads wouldn't go down well.
|
||||
s_exception_handler_mutex.lock();
|
||||
|
||||
// Prevent recursive exception filtering.
|
||||
HandlerResult result = HandlerResult::ExecuteNextHandler;
|
||||
if (!s_in_exception_handler)
|
||||
{
|
||||
s_in_exception_handler = true;
|
||||
result = HandlePageFault(exception_pc, exception_address, is_write);
|
||||
s_in_exception_handler = false;
|
||||
}
|
||||
|
||||
s_exception_handler_mutex.unlock();
|
||||
|
||||
// Resumes execution right where we left off (re-executes instruction that caused the SIGSEGV).
|
||||
if (result == HandlerResult::ContinueExecution)
|
||||
return;
|
||||
|
||||
// We couldn't handle it. Pass it off to the crash dumper.
|
||||
CrashHandler::CrashSignalHandler(sig, info, ctx);
|
||||
}
|
||||
|
||||
bool PageFaultHandler::Install(Error* error)
|
||||
{
|
||||
std::unique_lock lock(s_exception_handler_mutex);
|
||||
pxAssertRel(!s_installed, "Page fault handler has already been installed.");
|
||||
|
||||
struct sigaction sa;
|
||||
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_SIGINFO | SA_NODEFER;
|
||||
sa.sa_sigaction = SignalHandler;
|
||||
|
||||
if (sigaction(SIGSEGV, &sa, nullptr) != 0)
|
||||
{
|
||||
Error::SetErrno(error, "sigaction() for SIGSEGV failed: ", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef _M_ARM64
|
||||
// We can get SIGBUS on ARM64.
|
||||
if (sigaction(SIGBUS, &sa, nullptr) != 0)
|
||||
{
|
||||
Error::SetErrno(error, "sigaction() for SIGBUS failed: ", errno);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
s_installed = true;
|
||||
return true;
|
||||
}
|
||||
380
common/Linux/LnxMisc.cpp
Normal file
380
common/Linux/LnxMisc.cpp
Normal file
@@ -0,0 +1,380 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 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>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
|
||||
#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()
|
||||
{
|
||||
return 1000000000; // unix measures in nanoseconds
|
||||
}
|
||||
|
||||
u64 GetCPUTicks()
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (static_cast<u64>(ts.tv_sec) * 1000000000ULL) + ts.tv_nsec;
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
struct timespec ts;
|
||||
ts.tv_sec = static_cast<time_t>(ticks / 1000000000ULL);
|
||||
ts.tv_nsec = static_cast<long>(ticks % 1000000000ULL);
|
||||
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, nullptr);
|
||||
}
|
||||
348
common/Linux/LnxThreads.cpp
Normal file
348
common/Linux/LnxThreads.cpp
Normal file
@@ -0,0 +1,348 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "common/Threading.h"
|
||||
#include "common/Assertions.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#if defined(__linux__)
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sched.h>
|
||||
|
||||
// glibc < v2.30 doesn't define gettid...
|
||||
#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 30
|
||||
#include <sys/syscall.h>
|
||||
#define gettid() syscall(SYS_gettid)
|
||||
#endif
|
||||
#else
|
||||
#include <pthread_np.h>
|
||||
#endif
|
||||
|
||||
// Note: assuming multicore is safer because it forces the interlocked routines to use
|
||||
// the LOCK prefix. The prefix works on single core CPUs fine (but is slow), but not
|
||||
// having the LOCK prefix is very bad indeed.
|
||||
|
||||
__forceinline void Threading::Timeslice()
|
||||
{
|
||||
sched_yield();
|
||||
}
|
||||
|
||||
// For use in spin/wait loops, Acts as a hint to Intel CPUs and should, in theory
|
||||
// improve performance and reduce cpu power consumption.
|
||||
__forceinline void Threading::SpinWait()
|
||||
{
|
||||
// If this doesn't compile you can just comment it out (it only serves as a
|
||||
// performance hint and isn't required).
|
||||
#if defined(_M_X86)
|
||||
__asm__("pause");
|
||||
#elif defined(_M_ARM64)
|
||||
__asm__ __volatile__("isb");
|
||||
#endif
|
||||
}
|
||||
|
||||
__forceinline void Threading::EnableHiresScheduler()
|
||||
{
|
||||
// Don't know if linux has a customizable scheduler resolution like Windows (doubtful)
|
||||
}
|
||||
|
||||
__forceinline void Threading::DisableHiresScheduler()
|
||||
{
|
||||
}
|
||||
|
||||
// Unit of time of GetThreadCpuTime/GetCpuTime
|
||||
u64 Threading::GetThreadTicksPerSecond()
|
||||
{
|
||||
return 1000000;
|
||||
}
|
||||
|
||||
// Helper function to get either either the current cpu usage
|
||||
// in called thread or in id thread
|
||||
static u64 get_thread_time(uptr id = 0)
|
||||
{
|
||||
clockid_t cid;
|
||||
if (id)
|
||||
{
|
||||
int err = pthread_getcpuclockid((pthread_t)id, &cid);
|
||||
if (err)
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
cid = CLOCK_THREAD_CPUTIME_ID;
|
||||
}
|
||||
|
||||
struct timespec ts;
|
||||
int err = clock_gettime(cid, &ts);
|
||||
if (err)
|
||||
return 0;
|
||||
|
||||
return (u64)ts.tv_sec * (u64)1e6 + (u64)ts.tv_nsec / (u64)1e3;
|
||||
}
|
||||
|
||||
// Returns the current timestamp (not relative to a real world clock)
|
||||
u64 Threading::GetThreadCpuTime()
|
||||
{
|
||||
return get_thread_time();
|
||||
}
|
||||
|
||||
Threading::ThreadHandle::ThreadHandle() = default;
|
||||
|
||||
Threading::ThreadHandle::ThreadHandle(const ThreadHandle& handle)
|
||||
: m_native_handle(handle.m_native_handle)
|
||||
#ifdef __linux__
|
||||
, m_native_id(handle.m_native_id)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
Threading::ThreadHandle::ThreadHandle(ThreadHandle&& handle)
|
||||
: m_native_handle(handle.m_native_handle)
|
||||
#ifdef __linux__
|
||||
, m_native_id(handle.m_native_id)
|
||||
#endif
|
||||
{
|
||||
handle.m_native_handle = nullptr;
|
||||
#ifdef __linux__
|
||||
handle.m_native_id = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
Threading::ThreadHandle::~ThreadHandle() = default;
|
||||
|
||||
Threading::ThreadHandle Threading::ThreadHandle::GetForCallingThread()
|
||||
{
|
||||
ThreadHandle ret;
|
||||
ret.m_native_handle = (void*)pthread_self();
|
||||
#ifdef __linux__
|
||||
ret.m_native_id = gettid();
|
||||
#endif
|
||||
return ret;
|
||||
}
|
||||
|
||||
Threading::ThreadHandle& Threading::ThreadHandle::operator=(ThreadHandle&& handle)
|
||||
{
|
||||
m_native_handle = handle.m_native_handle;
|
||||
handle.m_native_handle = nullptr;
|
||||
#ifdef __linux__
|
||||
m_native_id = handle.m_native_id;
|
||||
handle.m_native_id = 0;
|
||||
#endif
|
||||
return *this;
|
||||
}
|
||||
|
||||
Threading::ThreadHandle& Threading::ThreadHandle::operator=(const ThreadHandle& handle)
|
||||
{
|
||||
m_native_handle = handle.m_native_handle;
|
||||
#ifdef __linux__
|
||||
m_native_id = handle.m_native_id;
|
||||
#endif
|
||||
return *this;
|
||||
}
|
||||
|
||||
u64 Threading::ThreadHandle::GetCPUTime() const
|
||||
{
|
||||
return m_native_handle ? get_thread_time((uptr)m_native_handle) : 0;
|
||||
}
|
||||
|
||||
bool Threading::ThreadHandle::SetAffinity(u64 processor_mask) const
|
||||
{
|
||||
#if defined(__linux__)
|
||||
cpu_set_t set;
|
||||
CPU_ZERO(&set);
|
||||
|
||||
if (processor_mask != 0)
|
||||
{
|
||||
for (u32 i = 0; i < 64; i++)
|
||||
{
|
||||
if (processor_mask & (static_cast<u64>(1) << i))
|
||||
{
|
||||
CPU_SET(i, &set);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
long num_processors = sysconf(_SC_NPROCESSORS_CONF);
|
||||
for (long i = 0; i < num_processors; i++)
|
||||
{
|
||||
CPU_SET(i, &set);
|
||||
}
|
||||
}
|
||||
|
||||
return sched_setaffinity((pid_t)m_native_id, sizeof(set), &set) >= 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
Threading::Thread::Thread() = default;
|
||||
|
||||
Threading::Thread::Thread(Thread&& thread)
|
||||
: ThreadHandle(thread)
|
||||
, m_stack_size(thread.m_stack_size)
|
||||
{
|
||||
thread.m_stack_size = 0;
|
||||
}
|
||||
|
||||
Threading::Thread::Thread(EntryPoint func)
|
||||
: ThreadHandle()
|
||||
{
|
||||
if (!Start(std::move(func)))
|
||||
pxFailRel("Failed to start implicitly started thread.");
|
||||
}
|
||||
|
||||
Threading::Thread::~Thread()
|
||||
{
|
||||
pxAssertRel(!m_native_handle, "Thread should be detached or joined at destruction");
|
||||
}
|
||||
|
||||
void Threading::Thread::SetStackSize(u32 size)
|
||||
{
|
||||
pxAssertRel(!m_native_handle, "Can't change the stack size on a started thread");
|
||||
m_stack_size = size;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
// For Linux, we have to do a bit of trickery here to get the thread's ID back from
|
||||
// the thread itself, because it's not part of pthreads. We use a semaphore to signal
|
||||
// when the thread has started, and filled in thread_id_ptr.
|
||||
struct ThreadProcParameters
|
||||
{
|
||||
Threading::Thread::EntryPoint func;
|
||||
Threading::KernelSemaphore* start_semaphore;
|
||||
unsigned int* thread_id_ptr;
|
||||
};
|
||||
|
||||
void* Threading::Thread::ThreadProc(void* param)
|
||||
{
|
||||
std::unique_ptr<ThreadProcParameters> entry(static_cast<ThreadProcParameters*>(param));
|
||||
*entry->thread_id_ptr = gettid();
|
||||
entry->start_semaphore->Post();
|
||||
entry->func();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Threading::Thread::Start(EntryPoint func)
|
||||
{
|
||||
pxAssertRel(!m_native_handle, "Can't start an already-started thread");
|
||||
|
||||
KernelSemaphore start_semaphore;
|
||||
std::unique_ptr<ThreadProcParameters> params(std::make_unique<ThreadProcParameters>());
|
||||
params->func = std::move(func);
|
||||
params->start_semaphore = &start_semaphore;
|
||||
params->thread_id_ptr = &m_native_id;
|
||||
|
||||
pthread_attr_t attrs;
|
||||
bool has_attributes = false;
|
||||
|
||||
if (m_stack_size != 0)
|
||||
{
|
||||
has_attributes = true;
|
||||
pthread_attr_init(&attrs);
|
||||
}
|
||||
if (m_stack_size != 0)
|
||||
pthread_attr_setstacksize(&attrs, m_stack_size);
|
||||
|
||||
pthread_t handle;
|
||||
const int res = pthread_create(&handle, has_attributes ? &attrs : nullptr, ThreadProc, params.get());
|
||||
if (res != 0)
|
||||
return false;
|
||||
|
||||
// wait until it sets our native id
|
||||
start_semaphore.Wait();
|
||||
|
||||
// thread started, it'll release the memory
|
||||
m_native_handle = (void*)handle;
|
||||
params.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void* Threading::Thread::ThreadProc(void* param)
|
||||
{
|
||||
std::unique_ptr<EntryPoint> entry(static_cast<EntryPoint*>(param));
|
||||
(*entry.get())();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Threading::Thread::Start(EntryPoint func)
|
||||
{
|
||||
pxAssertRel(!m_native_handle, "Can't start an already-started thread");
|
||||
|
||||
std::unique_ptr<EntryPoint> func_clone(std::make_unique<EntryPoint>(std::move(func)));
|
||||
|
||||
pthread_attr_t attrs;
|
||||
bool has_attributes = false;
|
||||
|
||||
if (m_stack_size != 0)
|
||||
{
|
||||
has_attributes = true;
|
||||
pthread_attr_init(&attrs);
|
||||
}
|
||||
if (m_stack_size != 0)
|
||||
pthread_attr_setstacksize(&attrs, m_stack_size);
|
||||
|
||||
pthread_t handle;
|
||||
const int res = pthread_create(&handle, has_attributes ? &attrs : nullptr, ThreadProc, func_clone.get());
|
||||
if (res != 0)
|
||||
return false;
|
||||
|
||||
// thread started, it'll release the memory
|
||||
m_native_handle = (void*)handle;
|
||||
func_clone.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void Threading::Thread::Detach()
|
||||
{
|
||||
pxAssertRel(m_native_handle, "Can't detach without a thread");
|
||||
pthread_detach((pthread_t)m_native_handle);
|
||||
m_native_handle = nullptr;
|
||||
#ifdef __linux__
|
||||
m_native_id = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Threading::Thread::Join()
|
||||
{
|
||||
pxAssertRel(m_native_handle, "Can't join without a thread");
|
||||
void* retval;
|
||||
const int res = pthread_join((pthread_t)m_native_handle, &retval);
|
||||
if (res != 0)
|
||||
pxFailRel("pthread_join() for thread join failed");
|
||||
|
||||
m_native_handle = nullptr;
|
||||
#ifdef __linux__
|
||||
m_native_id = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
Threading::ThreadHandle& Threading::Thread::operator=(Thread&& thread)
|
||||
{
|
||||
ThreadHandle::operator=(thread);
|
||||
m_stack_size = thread.m_stack_size;
|
||||
thread.m_stack_size = 0;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Threading::SetNameOfCurrentThread(const char* name)
|
||||
{
|
||||
#if defined(__linux__)
|
||||
// Extract of manpage: "The name can be up to 16 bytes long, and should be
|
||||
// null-terminated if it contains fewer bytes."
|
||||
prctl(PR_SET_NAME, name, 0, 0, 0);
|
||||
#elif defined(__unix__)
|
||||
pthread_set_name_np(pthread_self(), name);
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user