First Commit

This commit is contained in:
2025-11-18 14:18:26 -07:00
parent 33eb6e3707
commit 27277ec342
6106 changed files with 3571167 additions and 0 deletions

50
common/AlignedMalloc.cpp Normal file
View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// This module contains implementations of _aligned_malloc for platforms that don't have
// it built into their CRT/libc.
#if !defined(_WIN32)
#include "common/AlignedMalloc.h"
#include "common/Assertions.h"
#include <algorithm>
#include <cstdlib>
void* _aligned_malloc(size_t size, size_t align)
{
pxAssert(align < 0x10000);
#if defined(__USE_ISOC11) && !defined(ASAN_WORKAROUND) // not supported yet on gcc 4.9
return aligned_alloc(align, size);
#else
#ifdef __APPLE__
// MacOS has a bug where posix_memalign is ridiculously slow on unaligned sizes
// This especially bad on M1s for some reason
size = (size + align - 1) & ~(align - 1);
#endif
void* result = 0;
posix_memalign(&result, align, size);
return result;
#endif
}
void* pcsx2_aligned_realloc(void* handle, size_t new_size, size_t align, size_t old_size)
{
pxAssert(align < 0x10000);
void* newbuf = _aligned_malloc(new_size, align);
if (newbuf != NULL && handle != NULL)
{
memcpy(newbuf, handle, std::min(old_size, new_size));
_aligned_free(handle);
}
return newbuf;
}
__fi void _aligned_free(void* pmem)
{
free(pmem);
}
#endif

32
common/AlignedMalloc.h Normal file
View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <cstring>
#include <cstdlib>
#include <new> // std::bad_alloc
#include <memory>
#include <type_traits>
#include <utility>
#ifdef _MSC_VER
#include <malloc.h>
#endif
// Implementation note: all known implementations of _aligned_free check the pointer for
// NULL status (our implementation under GCC, and microsoft's under MSVC), so no need to
// do it here.
#define safe_aligned_free(ptr) \
((void)(_aligned_free(ptr), (ptr) = NULL))
// aligned_malloc: Implement/declare linux equivalents here!
#if !defined(_MSC_VER)
extern void* _aligned_malloc(size_t size, size_t align);
extern void* pcsx2_aligned_realloc(void* handle, size_t new_size, size_t align, size_t old_size);
extern void _aligned_free(void* pmem);
#else
#define pcsx2_aligned_realloc(handle, new_size, align, old_size) \
_aligned_realloc(handle, new_size, align)
#endif

120
common/Assertions.cpp Normal file
View File

@@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Assertions.h"
#include "CrashHandler.h"
#include "HostSys.h"
#include "Threading.h"
#include <mutex>
#ifdef _WIN32
#include "RedtapeWindows.h"
#include <intrin.h>
#include <tlhelp32.h>
#endif
#ifdef __UNIX__
#include <signal.h>
#endif
static std::mutex s_assertion_failed_mutex;
static inline void FreezeThreads(void** handle)
{
#if defined(_WIN32)
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
THREADENTRY32 threadEntry;
if (Thread32First(snapshot, &threadEntry))
{
do
{
if (threadEntry.th32ThreadID == GetCurrentThreadId())
continue;
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, threadEntry.th32ThreadID);
if (hThread != nullptr)
{
SuspendThread(hThread);
CloseHandle(hThread);
}
} while (Thread32Next(snapshot, &threadEntry));
}
}
*handle = static_cast<void*>(snapshot);
#else
* handle = nullptr;
#endif
}
static inline void ResumeThreads(void* handle)
{
#if defined(_WIN32)
if (handle != INVALID_HANDLE_VALUE)
{
THREADENTRY32 threadEntry;
if (Thread32First(reinterpret_cast<HANDLE>(handle), &threadEntry))
{
do
{
if (threadEntry.th32ThreadID == GetCurrentThreadId())
continue;
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, threadEntry.th32ThreadID);
if (hThread != nullptr)
{
ResumeThread(hThread);
CloseHandle(hThread);
}
} while (Thread32Next(reinterpret_cast<HANDLE>(handle), &threadEntry));
}
CloseHandle(reinterpret_cast<HANDLE>(handle));
}
#else
#endif
}
void pxOnAssertFail(const char* file, int line, const char* func, const char* msg)
{
std::unique_lock guard(s_assertion_failed_mutex);
void* handle;
FreezeThreads(&handle);
char full_msg[512];
std::snprintf(full_msg, sizeof(full_msg), "%s:%d: assertion failed in function %s: %s\n", file, line, func, msg);
#if defined(_WIN32)
HANDLE error_handle = GetStdHandle(STD_ERROR_HANDLE);
if (error_handle != INVALID_HANDLE_VALUE)
WriteConsoleA(GetStdHandle(STD_ERROR_HANDLE), full_msg, static_cast<DWORD>(std::strlen(full_msg)), NULL, NULL);
OutputDebugStringA(full_msg);
std::snprintf(
full_msg, sizeof(full_msg),
"Assertion failed in function %s (%s:%d):\n\n%s\n\nPress Abort to exit, Retry to break to debugger, or Ignore to attempt to continue.",
func, file, line, msg);
int result = MessageBoxA(NULL, full_msg, NULL, MB_ABORTRETRYIGNORE | MB_ICONERROR);
if (result == IDRETRY)
{
__debugbreak();
}
else if (result != IDIGNORE)
{
// try to save a crash dump before exiting
CrashHandler::WriteDumpForCaller();
TerminateProcess(GetCurrentProcess(), 0xBAADC0DE);
}
#else
fputs(full_msg, stderr);
fputs("\nAborting application.\n", stderr);
fflush(stderr);
AbortWithMessage(full_msg);
#endif
ResumeThreads(handle);
}

51
common/Assertions.h Normal file
View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Defs.h"
#ifndef __pxFUNCTION__
#if defined(__GNUG__)
#define __pxFUNCTION__ __PRETTY_FUNCTION__
#else
#define __pxFUNCTION__ __FUNCTION__
#endif
#endif
// pxAssertRel - assertion check even in Release builds.
// pxFailRel - aborts program even in Release builds.
//
// pxAssert[Msg] - assertion check only in Debug/Devel builds, noop in Release.
// pxAssume[Msg] - assertion check in Debug/Devel builds, optimization hint in Release builds.
// pxFail - aborts program only in Debug/Devel builds, noop in Release.
extern void pxOnAssertFail(const char* file, int line, const char* func, const char* msg);
#define pxAssertRel(cond, msg) do { if (!(cond)) [[unlikely]] { pxOnAssertFail(__FILE__, __LINE__, __pxFUNCTION__, msg); } } while(0)
#define pxFailRel(msg) pxOnAssertFail(__FILE__, __LINE__, __pxFUNCTION__, msg)
#if defined(PCSX2_DEBUG) || defined(PCSX2_DEVBUILD)
#define pxAssertMsg(cond, msg) pxAssertRel(cond, msg)
#define pxAssumeMsg(cond, msg) pxAssertRel(cond, msg)
#define pxFail(msg) pxFailRel(msg)
#else
#define pxAssertMsg(cond, msg) ((void)0)
#define pxAssumeMsg(cond, msg) ASSUME(cond)
#define pxFail(msg) ((void)0)
#endif
#define pxAssert(cond) pxAssertMsg(cond, #cond)
#define pxAssume(cond) pxAssumeMsg(cond, #cond)
// jNO_DEFAULT -- disables the default case in a switch, which improves switch optimization.
#define jNO_DEFAULT \
default: \
{ \
pxAssumeMsg(false, "Incorrect usage of jNO_DEFAULT detected (default case is not unreachable!)"); \
break; \
}

100
common/BitUtils.h Normal file
View File

@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Defs.h"
#include <bit>
#include <cstring>
#ifdef _MSC_VER
#include <intrin.h>
#else
static inline int _BitScanReverse(unsigned long* const Index, const unsigned long Mask)
{
if (Mask == 0)
return 0;
// For some reason, clang won't emit bsr if we use std::countl_zeros()...
*Index = 31 - __builtin_clz(Mask);
return 1;
}
#endif
namespace Common
{
template <typename T>
static constexpr __fi bool IsAligned(T value, unsigned int alignment)
{
return (value % static_cast<T>(alignment)) == 0;
}
template <typename T>
static constexpr __fi T AlignUp(T value, unsigned int alignment)
{
return (value + static_cast<T>(alignment - 1)) / static_cast<T>(alignment) * static_cast<T>(alignment);
}
template <typename T>
static constexpr __fi T AlignDown(T value, unsigned int alignment)
{
return value / static_cast<T>(alignment) * static_cast<T>(alignment);
}
template <typename T>
static constexpr __fi bool IsAlignedPow2(T value, unsigned int alignment)
{
return (value & static_cast<T>(alignment - 1)) == 0;
}
template <typename T>
static constexpr __fi T AlignUpPow2(T value, unsigned int alignment)
{
return (value + static_cast<T>(alignment - 1)) & static_cast<T>(~static_cast<T>(alignment - 1));
}
template <typename T>
static constexpr __fi T AlignDownPow2(T value, unsigned int alignment)
{
return value & static_cast<T>(~static_cast<T>(alignment - 1));
}
template <typename T>
static constexpr T PageAlign(T size)
{
static_assert(std::has_single_bit(__pagesize), "Page size is a power of 2");
return Common::AlignUpPow2(size, __pagesize);
}
__fi static u32 CountLeadingSignBits(s32 n)
{
// If the sign bit is 1, we invert the bits to 0 for count-leading-zero.
if (n < 0)
n = ~n;
// If BSR is used directly, it would have an undefined value for 0.
if (n == 0)
return 32;
// Perform our count leading zero.
return std::countl_zero(static_cast<u32>(n));
}
} // namespace Common
template <typename T>
[[maybe_unused]] __fi static T GetBufferT(const u8* buffer, u32 offset)
{
T value;
std::memcpy(&value, buffer + offset, sizeof(value));
return value;
}
[[maybe_unused]] __fi static u8 GetBufferU8(const u8* buffer, u32 offset) { return GetBufferT<u8>(buffer, offset); }
[[maybe_unused]] __fi static u16 GetBufferU16(const u8* buffer, u32 offset) { return GetBufferT<u16>(buffer, offset); }
[[maybe_unused]] __fi static u32 GetBufferU32(const u8* buffer, u32 offset) { return GetBufferT<u32>(buffer, offset); }
[[maybe_unused]] __fi static u64 GetBufferU64(const u8* buffer, u32 offset) { return GetBufferT<u64>(buffer, offset); }

44
common/ByteSwap.h Normal file
View File

@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <cstdint>
#include <type_traits>
#ifdef _MSC_VER
#include <stdlib.h>
#endif
template <typename T>
T ByteSwap(T value)
{
if constexpr (std::is_signed_v<T>)
{
return static_cast<T>(ByteSwap(std::make_unsigned_t<T>(value)));
}
else if constexpr (std::is_same_v<T, std::uint16_t>)
{
#ifdef _MSC_VER
return _byteswap_ushort(value);
#else
return __builtin_bswap16(value);
#endif
}
else if constexpr (std::is_same_v<T, std::uint32_t>)
{
#ifdef _MSC_VER
return _byteswap_ulong(value);
#else
return __builtin_bswap32(value);
#endif
}
else if constexpr (std::is_same_v<T, std::uint64_t>)
{
#ifdef _MSC_VER
return _byteswap_uint64(value);
#else
return __builtin_bswap64(value);
#endif
}
}

224
common/CMakeLists.txt Normal file
View File

@@ -0,0 +1,224 @@
# Check that people use the good file
if(NOT TOP_CMAKE_WAS_SOURCED)
message(FATAL_ERROR "
You did not 'cmake' the good CMakeLists.txt file. Use the one in the top dir.
It is advice to delete all wrongly generated cmake stuff => CMakeFiles & CMakeCache.txt")
endif(NOT TOP_CMAKE_WAS_SOURCED)
add_library(common)
# x86emitter sources
target_sources(common PRIVATE
AlignedMalloc.cpp
Assertions.cpp
Console.cpp
CrashHandler.cpp
DynamicLibrary.cpp
Error.cpp
FastJmp.cpp
FileSystem.cpp
HostSys.cpp
Image.cpp
HTTPDownloader.cpp
MemorySettingsInterface.cpp
MD5Digest.cpp
PrecompiledHeader.cpp
Perf.cpp
ProgressCallback.cpp
ReadbackSpinManager.cpp
Semaphore.cpp
SettingsWrapper.cpp
SmallString.cpp
StringUtil.cpp
TextureDecompress.cpp
Timer.cpp
WAVWriter.cpp
WindowInfo.cpp
)
# x86emitter headers
target_sources(common PRIVATE
AlignedMalloc.h
Assertions.h
boost_spsc_queue.hpp
BitUtils.h
ByteSwap.h
Console.h
CrashHandler.h
DynamicLibrary.h
Easing.h
EnumOps.h
Error.h
FPControl.h
FastJmp.h
FileSystem.h
HashCombine.h
HostSys.h
HeterogeneousContainers.h
Image.h
LRUCache.h
HeapArray.h
HTTPDownloader.h
MemorySettingsInterface.h
MD5Digest.h
MRCHelpers.h
Path.h
PrecompiledHeader.h
ProgressCallback.h
ReadbackSpinManager.h
RedtapeWilCom.h
RedtapeWindows.h
ScopedGuard.h
SettingsInterface.h
SettingsWrapper.h
SingleRegisterTypes.h
SmallString.h
StringUtil.h
Timer.h
TextureDecompress.h
Threading.h
VectorIntrin.h
WAVWriter.h
WindowInfo.h
WrappedMemCopy.h
)
if(_M_X86)
target_sources(common PRIVATE
emitter/avx.cpp
emitter/bmi.cpp
emitter/fpu.cpp
emitter/groups.cpp
emitter/jmp.cpp
emitter/legacy.cpp
emitter/legacy_sse.cpp
emitter/movs.cpp
emitter/simd.cpp
emitter/x86emitter.cpp
)
target_sources(common PRIVATE
emitter/implement/dwshift.h
emitter/implement/group1.h
emitter/implement/group2.h
emitter/implement/group3.h
emitter/implement/helpers.h
emitter/implement/incdec.h
emitter/implement/jmpcall.h
emitter/implement/movs.h
emitter/implement/simd_arithmetic.h
emitter/implement/simd_comparisons.h
emitter/implement/simd_helpers.h
emitter/implement/simd_moremovs.h
emitter/implement/simd_shufflepack.h
emitter/implement/simd_templated_helpers.h
emitter/implement/test.h
emitter/implement/xchg.h
emitter/instructions.h
emitter/internal.h
emitter/legacy_instructions.h
emitter/legacy_internal.h
emitter/legacy_types.h
emitter/x86emitter.h
emitter/x86types.h
)
endif()
if(WIN32)
enable_language(ASM_MASM)
target_sources(common PRIVATE
FastJmp.asm
HTTPDownloaderWinHTTP.cpp
HTTPDownloaderWinHTTP.h
StackWalker.cpp
StackWalker.h
Windows/WinThreads.cpp
Windows/WinHostSys.cpp
Windows/WinMisc.cpp
)
target_link_libraries(common PUBLIC
WIL::WIL
winmm
pathcch
)
elseif(APPLE)
target_sources(common PRIVATE
CocoaTools.mm
CocoaTools.h
Darwin/DarwinThreads.cpp
Darwin/DarwinMisc.cpp
Darwin/DarwinMisc.h
)
target_compile_options(common PRIVATE -fobjc-arc)
target_link_options(common PRIVATE -fobjc-link-runtime)
target_link_libraries(common PRIVATE
"-framework Foundation"
"-framework IOKit"
)
else()
target_sources(common PRIVATE
Linux/LnxHostSys.cpp
Linux/LnxThreads.cpp
Linux/LnxMisc.cpp
)
target_include_directories(common PRIVATE
${DBUS_INCLUDE_DIRS}
)
target_link_libraries(common PRIVATE
${DBUS_LINK_LIBRARIES}
X11::X11
X11::Xrandr
X11::Xi
)
if(USE_BACKTRACE)
target_compile_definitions(common PRIVATE "HAS_LIBBACKTRACE=1")
target_link_libraries(common PRIVATE libbacktrace::libbacktrace)
endif()
if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
target_link_libraries(common PRIVATE cpuinfo)
endif()
endif()
set_source_files_properties(PrecompiledHeader.cpp PROPERTIES HEADER_FILE_ONLY TRUE)
if(USE_VTUNE)
target_link_libraries(common PUBLIC Vtune::Vtune)
endif()
if (USE_GCC AND CMAKE_INTERPROCEDURAL_OPTIMIZATION)
# GCC LTO doesn't work with asm statements
set_source_files_properties(FastJmp.cpp PROPERTIES COMPILE_FLAGS -fno-lto)
endif()
if(NOT WIN32)
# libcurl-based HTTPDownloader
target_sources(common PRIVATE
HTTPDownloaderCurl.cpp
HTTPDownloaderCurl.h
)
target_link_libraries(common PRIVATE
CURL::libcurl
)
endif()
target_link_libraries(common PRIVATE
${LIBC_LIBRARIES}
JPEG::JPEG
PNG::PNG
WebP::libwebp
)
target_link_libraries(common PUBLIC
fmt::fmt
fast_float
)
fixup_file_properties(common)
target_compile_features(common PUBLIC cxx_std_17)
target_include_directories(common PUBLIC ../3rdparty/include ../)
target_compile_definitions(common PUBLIC "${PCSX2_DEFS}")
target_compile_options(common PRIVATE "${PCSX2_WARNINGS}")
if(COMMAND target_precompile_headers)
target_precompile_headers(common PRIVATE PrecompiledHeader.h)
endif()

49
common/CocoaTools.h Normal file
View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#ifdef __APPLE__
#include <string>
#include <optional>
struct WindowInfo;
/// Helper functions for things that need Objective-C
namespace CocoaTools
{
bool CreateMetalLayer(WindowInfo* wi);
void DestroyMetalLayer(WindowInfo* wi);
std::optional<float> GetViewRefreshRate(const WindowInfo& wi);
/// Add a handler to be run when macOS changes between dark and light themes
void AddThemeChangeHandler(void* ctx, void(handler)(void* ctx));
/// Remove a handler previously added using AddThemeChangeHandler with the given context
void RemoveThemeChangeHandler(void* ctx);
/// Mark an NSMenu as the help menu
void MarkHelpMenu(void* menu);
/// Returns the bundle path.
std::optional<std::string> GetBundlePath();
/// Get the bundle path to the actual application without any translocation fun
std::optional<std::string> GetNonTranslocatedBundlePath();
/// Move the given file to the trash, and return the path to its new location
std::optional<std::string> MoveToTrash(std::string_view file);
/// Launch the given application once this one quits
bool DelayedLaunch(std::string_view file);
/// Open a Finder window to the given URL
bool ShowInFinder(std::string_view file);
/// Get the path to the resources directory of the current application
std::optional<std::string> GetResourcePath();
/// Create a window
void* CreateWindow(std::string_view title, uint32_t width, uint32_t height);
/// Destroy a window
void DestroyWindow(void* window);
/// Make a WindowInfo from the given window
void GetWindowInfoFromWindow(WindowInfo* wi, void* window);
/// Run cocoa event loop
void RunCocoaEventLoop(bool wait_forever = false);
/// Posts an event to the main telling `RunCocoaEventLoop(true)` to exit
void StopMainThreadEventLoop();
}
#endif // __APPLE__

326
common/CocoaTools.mm Normal file
View File

@@ -0,0 +1,326 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#if ! __has_feature(objc_arc)
#error "Compile this with -fobjc-arc"
#endif
#include "CocoaTools.h"
#include "Console.h"
#include "HostSys.h"
#include "WindowInfo.h"
#include <dlfcn.h>
#include <mutex>
#include <vector>
#include <Cocoa/Cocoa.h>
#include <QuartzCore/QuartzCore.h>
// MARK: - Metal Layers
static NSString*_Nonnull NSStringFromStringView(std::string_view sv)
{
return [[NSString alloc] initWithBytes:sv.data() length:sv.size() encoding:NSUTF8StringEncoding];
}
bool CocoaTools::CreateMetalLayer(WindowInfo* wi)
{
if (![NSThread isMainThread])
{
bool ret;
dispatch_sync(dispatch_get_main_queue(), [&ret, wi]{ ret = CreateMetalLayer(wi); });
return ret;
}
CAMetalLayer* layer = [CAMetalLayer layer];
if (!layer)
{
Console.Error("Failed to create Metal layer.");
return false;
}
NSView* view = (__bridge NSView*)wi->window_handle;
[view setWantsLayer:YES];
[view setLayer:layer];
[layer setContentsScale:[[[view window] screen] backingScaleFactor]];
// Store the layer pointer, that way MoltenVK doesn't call [NSView layer] outside the main thread.
wi->surface_handle = (__bridge_retained void*)layer;
return true;
}
void CocoaTools::DestroyMetalLayer(WindowInfo* wi)
{
if (![NSThread isMainThread])
{
dispatch_sync_f(dispatch_get_main_queue(), wi, [](void* ctx){ DestroyMetalLayer(static_cast<WindowInfo*>(ctx)); });
return;
}
NSView* view = (__bridge NSView*)wi->window_handle;
CAMetalLayer* layer = (__bridge_transfer CAMetalLayer*)wi->surface_handle;
if (!layer)
return;
wi->surface_handle = nullptr;
[view setLayer:nil];
[view setWantsLayer:NO];
}
std::optional<float> CocoaTools::GetViewRefreshRate(const WindowInfo& wi)
{
if (![NSThread isMainThread])
{
std::optional<float> ret;
dispatch_sync(dispatch_get_main_queue(), [&ret, wi]{ ret = GetViewRefreshRate(wi); });
return ret;
}
std::optional<float> ret;
NSView* const view = (__bridge NSView*)wi.window_handle;
const u32 did = [[[[[view window] screen] deviceDescription] valueForKey:@"NSScreenNumber"] unsignedIntValue];
if (CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did))
{
ret = CGDisplayModeGetRefreshRate(mode);
CGDisplayModeRelease(mode);
}
return ret;
}
// MARK: - Theme Change Handlers
@interface PCSX2KVOHelper : NSObject
- (void)addCallback:(void*)ctx run:(void(*)(void*))callback;
- (void)removeCallback:(void*)ctx;
@end
@implementation PCSX2KVOHelper
{
std::vector<std::pair<void*, void(*)(void*)>> _callbacks;
}
- (void)addCallback:(void*)ctx run:(void(*)(void*))callback
{
_callbacks.push_back(std::make_pair(ctx, callback));
}
- (void)removeCallback:(void*)ctx
{
auto new_end = std::remove_if(_callbacks.begin(), _callbacks.end(), [ctx](const auto& entry){
return ctx == entry.first;
});
_callbacks.erase(new_end, _callbacks.end());
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
for (const auto& callback : _callbacks)
callback.second(callback.first);
}
@end
static PCSX2KVOHelper* s_themeChangeHandler;
void CocoaTools::AddThemeChangeHandler(void* ctx, void(handler)(void* ctx))
{
assert([NSThread isMainThread]);
if (!s_themeChangeHandler)
{
s_themeChangeHandler = [[PCSX2KVOHelper alloc] init];
NSApplication* app = [NSApplication sharedApplication];
[app addObserver:s_themeChangeHandler
forKeyPath:@"effectiveAppearance"
options:0
context:nil];
}
[s_themeChangeHandler addCallback:ctx run:handler];
}
void CocoaTools::RemoveThemeChangeHandler(void* ctx)
{
assert([NSThread isMainThread]);
[s_themeChangeHandler removeCallback:ctx];
}
void CocoaTools::MarkHelpMenu(void* menu)
{
[NSApp setHelpMenu:(__bridge NSMenu*)menu];
}
// MARK: - Sound playback
bool Common::PlaySoundAsync(const char* path)
{
NSString* nspath = [[NSString alloc] initWithUTF8String:path];
NSSound* sound = [[NSSound alloc] initWithContentsOfFile:nspath byReference:YES];
return [sound play];
}
// MARK: - Updater
std::optional<std::string> CocoaTools::GetBundlePath()
{
std::optional<std::string> ret;
@autoreleasepool {
NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
if (url)
ret = std::string([url fileSystemRepresentation]);
}
return ret;
}
std::optional<std::string> CocoaTools::GetNonTranslocatedBundlePath()
{
// See https://objective-see.com/blog/blog_0x15.html
NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
if (!url)
return std::nullopt;
if (void* handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY))
{
auto IsTranslocatedURL = reinterpret_cast<Boolean(*)(CFURLRef path, bool* isTranslocated, CFErrorRef*__nullable error)>(dlsym(handle, "SecTranslocateIsTranslocatedURL"));
auto CreateOriginalPathForURL = reinterpret_cast<CFURLRef __nullable(*)(CFURLRef translocatedPath, CFErrorRef*__nullable error)>(dlsym(handle, "SecTranslocateCreateOriginalPathForURL"));
bool is_translocated = false;
if (IsTranslocatedURL)
IsTranslocatedURL((__bridge CFURLRef)url, &is_translocated, nullptr);
if (is_translocated)
{
if (CFURLRef actual = CreateOriginalPathForURL((__bridge CFURLRef)url, nullptr))
url = (__bridge_transfer NSURL*)actual;
}
dlclose(handle);
}
return std::string([url fileSystemRepresentation]);
}
std::optional<std::string> CocoaTools::MoveToTrash(std::string_view file)
{
NSURL* url = [NSURL fileURLWithPath:NSStringFromStringView(file)];
NSURL* new_url;
if (![[NSFileManager defaultManager] trashItemAtURL:url resultingItemURL:&new_url error:nil])
return std::nullopt;
return std::string([new_url fileSystemRepresentation]);
}
bool CocoaTools::DelayedLaunch(std::string_view file)
{
@autoreleasepool {
NSTask* task = [NSTask new];
[task setExecutableURL:[NSURL fileURLWithPath:@"/bin/sh"]];
[task setEnvironment:@{
@"WAITPID": [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]],
@"LAUNCHAPP": NSStringFromStringView(file),
}];
[task setArguments:@[@"-c", @"while /bin/ps -p $WAITPID > /dev/null; do /bin/sleep 0.1; done; exec /usr/bin/open \"$LAUNCHAPP\";"]];
return [task launchAndReturnError:nil];
}
}
// MARK: - Directory Services
bool CocoaTools::ShowInFinder(std::string_view file)
{
return [[NSWorkspace sharedWorkspace] selectFile:NSStringFromStringView(file)
inFileViewerRootedAtPath:@""];
}
std::optional<std::string> CocoaTools::GetResourcePath()
{ @autoreleasepool {
if (NSBundle* bundle = [NSBundle mainBundle])
{
NSString* rsrc = [bundle resourcePath];
NSString* root = [bundle bundlePath];
if ([rsrc isEqualToString:root])
rsrc = [rsrc stringByAppendingString:@"/resources"];
return [rsrc UTF8String];
}
return std::nullopt;
}}
// MARK: - GSRunner
void* CocoaTools::CreateWindow(std::string_view title, u32 width, u32 height)
{
if (!NSApp)
{
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp finishLaunching];
}
constexpr NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
NSScreen* mainScreen = [NSScreen mainScreen];
// Center the window on the screen, because why not
NSRect screenFrame = [mainScreen frame];
NSRect viewFrame = screenFrame;
viewFrame.size = NSMakeSize(width, height);
viewFrame.origin.x += (screenFrame.size.width - viewFrame.size.width) / 2;
viewFrame.origin.y += (screenFrame.size.height - viewFrame.size.height) / 2;
NSWindow* window = [[NSWindow alloc]
initWithContentRect:viewFrame
styleMask:style
backing:NSBackingStoreBuffered
defer:NO];
[window setTitle:NSStringFromStringView(title)];
[window makeKeyAndOrderFront:window];
return (__bridge_retained void*)window;
}
void CocoaTools::DestroyWindow(void* window)
{
(void)(__bridge_transfer NSWindow*)window;
}
void CocoaTools::GetWindowInfoFromWindow(WindowInfo* wi, void* cf_window)
{
if (cf_window)
{
NSWindow* window = (__bridge NSWindow*)cf_window;
float scale = [window backingScaleFactor];
NSView* view = [window contentView];
NSRect dims = [view frame];
wi->type = WindowInfo::Type::MacOS;
wi->window_handle = (__bridge void*)view;
wi->surface_width = dims.size.width * scale;
wi->surface_height = dims.size.height * scale;
wi->surface_scale = scale;
}
else
{
wi->type = WindowInfo::Type::Surfaceless;
}
}
static constexpr short STOP_EVENT_LOOP = 0x100;
void CocoaTools::RunCocoaEventLoop(bool forever)
{
NSDate* end = forever ? [NSDate distantFuture] : [NSDate distantPast];
[NSApplication sharedApplication]; // Ensure NSApp is initialized
while (true)
{ @autoreleasepool {
NSEvent* ev = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:end
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (!ev || ([ev type] == NSEventTypeApplicationDefined && [ev subtype] == STOP_EVENT_LOOP))
break;
[NSApp sendEvent:ev];
}}
}
void CocoaTools::StopMainThreadEventLoop()
{ @autoreleasepool {
NSEvent* ev = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:{}
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:STOP_EVENT_LOOP
data1:0
data2:0];
[NSApp postEvent:ev atStart:NO];
}}

510
common/Console.cpp Normal file
View File

@@ -0,0 +1,510 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/Console.h"
#include "common/Assertions.h"
#include "common/FileSystem.h"
#include "common/SmallString.h"
#include "common/Timer.h"
#include "fmt/format.h"
#include <mutex>
#include <vector>
#ifdef _WIN32
#include "common/RedtapeWindows.h"
#else
#include <unistd.h>
#endif
using namespace std::string_view_literals;
// Dummy objects, need to get rid of them...
ConsoleLogWriter<LOGLEVEL_INFO> Console;
ConsoleLogWriter<LOGLEVEL_DEV> DevCon;
#ifdef _DEBUG
ConsoleLogWriter<LOGLEVEL_DEBUG> DbgConWriter;
#else
NullLogWriter DbgConWriter;
#endif
#define TIMESTAMP_FORMAT_STRING "[{:10.4f}] "
#define TIMESTAMP_PRINTF_STRING "[%10.4f] "
namespace Log
{
static void WriteToConsole(LOGLEVEL level, ConsoleColors color, std::string_view message);
static void WriteToDebug(LOGLEVEL level, ConsoleColors color, std::string_view message);
static void WriteToFile(LOGLEVEL level, ConsoleColors color, std::string_view message);
static void UpdateMaxLevel();
static void ExecuteCallbacks(LOGLEVEL level, ConsoleColors color, std::string_view message);
static Common::Timer::Value s_start_timestamp = Common::Timer::GetCurrentValue();
static LOGLEVEL s_max_level = LOGLEVEL_NONE;
static LOGLEVEL s_console_level = LOGLEVEL_NONE;
static LOGLEVEL s_debug_level = LOGLEVEL_NONE;
static LOGLEVEL s_file_level = LOGLEVEL_NONE;
static LOGLEVEL s_host_level = LOGLEVEL_NONE;
static bool s_log_timestamps = true;
static FileSystem::ManagedCFilePtr s_file_handle;
static std::string s_file_path;
static std::mutex s_file_mutex;
static HostCallbackType s_host_callback;
#ifdef _WIN32
static HANDLE s_hConsoleStdIn = NULL;
static HANDLE s_hConsoleStdOut = NULL;
static HANDLE s_hConsoleStdErr = NULL;
#endif
} // namespace Log
float Log::GetCurrentMessageTime()
{
return static_cast<float>(Common::Timer::ConvertValueToSeconds(Common::Timer::GetCurrentValue() - s_start_timestamp));
}
__ri void Log::WriteToConsole(LOGLEVEL level, ConsoleColors color, std::string_view message)
{
static constexpr std::string_view s_ansi_color_codes[ConsoleColors_Count] = {
"\033[0m"sv, // default
"\033[30m\033[1m"sv, // black
"\033[32m"sv, // green
"\033[31m"sv, // red
"\033[34m"sv, // blue
"\033[35m"sv, // magenta
"\033[35m"sv, // orange (FIXME)
"\033[37m"sv, // gray
"\033[36m"sv, // cyan
"\033[33m"sv, // yellow
"\033[37m"sv, // white
"\033[30m\033[1m"sv, // strong black
"\033[31m\033[1m"sv, // strong red
"\033[32m\033[1m"sv, // strong green
"\033[34m\033[1m"sv, // strong blue
"\033[35m\033[1m"sv, // strong magenta
"\033[35m\033[1m"sv, // strong orange (FIXME)
"\033[37m\033[1m"sv, // strong gray
"\033[36m\033[1m"sv, // strong cyan
"\033[33m\033[1m"sv, // strong yellow
"\033[37m\033[1m"sv, // strong white
};
static constexpr size_t BUFFER_SIZE = 512;
SmallStackString<BUFFER_SIZE> buffer;
buffer.reserve(32 + message.length());
buffer.append(s_ansi_color_codes[color]);
if (s_log_timestamps)
buffer.append_format(TIMESTAMP_FORMAT_STRING, Log::GetCurrentMessageTime());
buffer.append(message);
buffer.append('\n');
#ifdef _WIN32
const HANDLE hOutput = (level <= LOGLEVEL_WARNING) ? s_hConsoleStdErr : s_hConsoleStdOut;
// Convert to UTF-16 first so Unicode characters display correctly. NT is going to do it anyway...
wchar_t wbuf[BUFFER_SIZE];
wchar_t* wmessage_buf = wbuf;
int wmessage_buflen = static_cast<int>(std::size(wbuf) - 1);
if (buffer.length() >= std::size(wbuf))
{
wmessage_buflen = static_cast<int>(buffer.length());
wmessage_buf = static_cast<wchar_t*>(std::malloc(static_cast<size_t>(wmessage_buflen) * sizeof(wchar_t)));
if (!wmessage_buf)
return;
}
const int wmessage_size = MultiByteToWideChar(CP_UTF8, 0, buffer.data(), static_cast<int>(buffer.length()), wmessage_buf, wmessage_buflen);
if (wmessage_size <= 0)
goto cleanup;
DWORD chars_written;
WriteConsoleW(hOutput, wmessage_buf, static_cast<DWORD>(wmessage_size), &chars_written, nullptr);
cleanup:
if (wmessage_buf != wbuf)
std::free(wmessage_buf);
#else
const int fd = (level <= LOGLEVEL_WARNING) ? STDERR_FILENO : STDOUT_FILENO;
write(fd, buffer.data(), buffer.length());
#endif
}
bool Log::IsConsoleOutputEnabled()
{
return (s_console_level > LOGLEVEL_NONE);
}
void Log::SetConsoleOutputLevel(LOGLEVEL level)
{
if (s_console_level == level)
return;
const bool was_enabled = (s_console_level > LOGLEVEL_NONE);
const bool now_enabled = (level > LOGLEVEL_NONE);
s_console_level = level;
UpdateMaxLevel();
if (was_enabled == now_enabled)
return;
// Worst that happens here is we write to a bad handle..
#if defined(_WIN32)
static constexpr auto enable_virtual_terminal_processing = [](HANDLE hConsole) {
DWORD old_mode;
if (!GetConsoleMode(hConsole, &old_mode))
return;
// already enabled?
if (old_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
return;
SetConsoleMode(hConsole, old_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
};
// On windows, no console is allocated by default on a windows based application
static bool console_was_allocated = false;
static HANDLE old_stdin = NULL;
static HANDLE old_stdout = NULL;
static HANDLE old_stderr = NULL;
if (now_enabled)
{
old_stdin = GetStdHandle(STD_INPUT_HANDLE);
old_stdout = GetStdHandle(STD_OUTPUT_HANDLE);
old_stderr = GetStdHandle(STD_ERROR_HANDLE);
if (!old_stdout)
{
// Attach to the parent console if we're running from a command window
if (!AttachConsole(ATTACH_PARENT_PROCESS) && !AllocConsole())
return;
s_hConsoleStdIn = GetStdHandle(STD_INPUT_HANDLE);
s_hConsoleStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
s_hConsoleStdErr = GetStdHandle(STD_ERROR_HANDLE);
enable_virtual_terminal_processing(s_hConsoleStdOut);
enable_virtual_terminal_processing(s_hConsoleStdErr);
std::FILE* fp;
freopen_s(&fp, "CONIN$", "r", stdin);
freopen_s(&fp, "CONOUT$", "w", stdout);
freopen_s(&fp, "CONOUT$", "w", stderr);
console_was_allocated = true;
}
else
{
s_hConsoleStdIn = old_stdin;
s_hConsoleStdOut = old_stdout;
s_hConsoleStdErr = old_stderr;
}
}
else
{
if (console_was_allocated)
{
console_was_allocated = false;
std::FILE* fp;
freopen_s(&fp, "NUL:", "w", stderr);
freopen_s(&fp, "NUL:", "w", stdout);
freopen_s(&fp, "NUL:", "w", stdin);
SetStdHandle(STD_ERROR_HANDLE, old_stderr);
SetStdHandle(STD_OUTPUT_HANDLE, old_stdout);
SetStdHandle(STD_INPUT_HANDLE, old_stdin);
s_hConsoleStdIn = NULL;
s_hConsoleStdOut = NULL;
s_hConsoleStdErr = NULL;
FreeConsole();
}
}
#endif
}
__ri void Log::WriteToDebug(LOGLEVEL level, ConsoleColors color, std::string_view message)
{
#ifdef _WIN32
static constexpr size_t BUFFER_SIZE = 512;
// Convert to UTF-16 first so Unicode characters display correctly. NT is going to do it anyway...
wchar_t wbuf[BUFFER_SIZE];
wchar_t* wmessage_buf = wbuf;
int wmessage_buflen = static_cast<int>(std::size(wbuf) - 1);
if (message.length() >= std::size(wbuf))
{
wmessage_buflen = static_cast<int>(message.length());
wmessage_buf = static_cast<wchar_t*>(std::malloc((static_cast<size_t>(wmessage_buflen) + 2) * sizeof(wchar_t)));
if (!wmessage_buf)
return;
}
int wmessage_size = 0;
if (!message.empty()) [[likely]]
{
wmessage_size = MultiByteToWideChar(CP_UTF8, 0, message.data(), static_cast<int>(message.length()), wmessage_buf, wmessage_buflen);
if (wmessage_size <= 0)
goto cleanup;
}
wmessage_buf[wmessage_size++] = L'\n';
wmessage_buf[wmessage_size++] = 0;
OutputDebugStringW(wmessage_buf);
cleanup:
if (wmessage_buf != wbuf)
std::free(wmessage_buf);
#endif
}
bool Log::IsDebugOutputAvailable()
{
#ifdef _WIN32
return IsDebuggerPresent();
#else
return false;
#endif
}
bool Log::IsDebugOutputEnabled()
{
return (s_console_level > LOGLEVEL_NONE);
}
void Log::SetDebugOutputLevel(LOGLEVEL level)
{
s_debug_level = level;
UpdateMaxLevel();
}
__ri void Log::WriteToFile(LOGLEVEL level, ConsoleColors color, std::string_view message)
{
std::unique_lock lock(s_file_mutex);
if (!s_file_handle) [[unlikely]]
return;
if (!message.empty()) [[likely]]
{
if (s_log_timestamps)
{
std::fprintf(s_file_handle.get(), TIMESTAMP_PRINTF_STRING "%.*s\n", GetCurrentMessageTime(),
static_cast<int>(message.size()), message.data());
}
else
{
std::fprintf(s_file_handle.get(), "%.*s\n", static_cast<int>(message.size()), message.data());
}
}
else
{
if (s_log_timestamps)
{
std::fprintf(s_file_handle.get(), TIMESTAMP_PRINTF_STRING "\n", GetCurrentMessageTime());
}
else
{
std::fputc('\n', s_file_handle.get());
}
}
std::fflush(s_file_handle.get());
}
bool Log::IsFileOutputEnabled()
{
return (s_file_level > LOGLEVEL_NONE);
}
bool Log::SetFileOutputLevel(LOGLEVEL level, std::string path)
{
std::unique_lock lock(s_file_mutex);
const bool was_enabled = (s_file_level > LOGLEVEL_NONE);
const bool new_enabled = (level > LOGLEVEL_NONE && !path.empty());
if (was_enabled != new_enabled || (new_enabled && path == s_file_path))
{
if (new_enabled)
{
if (!s_file_handle || s_file_path != path)
{
s_file_handle.reset();
s_file_handle = FileSystem::OpenManagedSharedCFile(path.c_str(), "wb", FileSystem::FileShareMode::DenyWrite);
if (s_file_handle)
{
s_file_path = std::move(path);
}
else
{
s_file_path = {};
if (IsConsoleOutputEnabled())
WriteToConsole(LOGLEVEL_ERROR, Color_StrongRed, TinyString::from_format("Failed to open log file '{}'", path));
}
}
}
else
{
s_file_handle.reset();
s_file_path = {};
}
}
s_file_level = s_file_handle ? level : LOGLEVEL_NONE;
UpdateMaxLevel();
return IsFileOutputEnabled();
}
std::FILE* Log::GetFileLogHandle()
{
std::unique_lock lock(s_file_mutex);
return s_file_handle.get();
}
bool Log::IsHostOutputEnabled()
{
return (s_host_level > LOGLEVEL_NONE);
}
void Log::SetHostOutputLevel(LOGLEVEL level, HostCallbackType callback)
{
s_host_callback = callback;
s_host_level = callback ? level : LOGLEVEL_NONE;
UpdateMaxLevel();
}
bool Log::AreTimestampsEnabled()
{
return s_log_timestamps;
}
void Log::SetTimestampsEnabled(bool enabled)
{
s_log_timestamps = enabled;
}
LOGLEVEL Log::GetMaxLevel()
{
return s_max_level;
}
__ri void Log::UpdateMaxLevel()
{
s_max_level = std::max(s_console_level, std::max(s_debug_level, std::max(s_file_level, s_host_level)));
}
void Log::ExecuteCallbacks(LOGLEVEL level, ConsoleColors color, std::string_view message)
{
// TODO: Cache the message time.
// Split newlines into separate messages.
std::string_view::size_type start_pos = 0;
if (std::string_view::size_type end_pos = message.find('\n'); end_pos != std::string::npos) [[unlikely]]
{
for (;;)
{
std::string_view message_line;
if (start_pos != end_pos)
message_line = message.substr(start_pos, (end_pos == std::string_view::npos) ? end_pos : end_pos - start_pos);
ExecuteCallbacks(level, color, message_line);
if (end_pos == std::string_view::npos)
return;
start_pos = end_pos + 1;
end_pos = message.find('\n', start_pos);
}
return;
}
pxAssert(level > LOGLEVEL_NONE);
if (level <= s_console_level)
WriteToConsole(level, color, message);
if (level <= s_debug_level)
WriteToDebug(level, color, message);
if (level <= s_file_level)
WriteToFile(level, color, message);
if (level <= s_host_level)
{
// double check in case of race here
const HostCallbackType callback = s_host_callback;
if (callback)
s_host_callback(level, color, message);
}
}
void Log::Write(LOGLEVEL level, ConsoleColors color, std::string_view message)
{
if (level > s_max_level)
return;
ExecuteCallbacks(level, color, message);
}
void Log::Writef(LOGLEVEL level, ConsoleColors color, const char* format, ...)
{
std::va_list ap;
va_start(ap, format);
Writev(level, color, format, ap);
va_end(ap);
}
void Log::Writev(LOGLEVEL level, ConsoleColors color, const char* format, va_list ap)
{
if (level > s_max_level)
return;
std::va_list ap_copy;
va_copy(ap_copy, ap);
#ifdef _WIN32
const u32 required_size = static_cast<u32>(_vscprintf(format, ap_copy));
#else
const u32 required_size = std::vsnprintf(nullptr, 0, format, ap_copy);
#endif
va_end(ap_copy);
if (required_size < 512)
{
char buffer[512];
const int len = std::vsnprintf(buffer, std::size(buffer), format, ap);
if (len > 0)
ExecuteCallbacks(level, color, std::string_view(buffer, static_cast<size_t>(len)));
}
else
{
char* buffer = new char[required_size + 1];
const int len = std::vsnprintf(buffer, required_size + 1, format, ap);
if (len > 0)
ExecuteCallbacks(level, color, std::string_view(buffer, static_cast<size_t>(len)));
delete[] buffer;
}
}
void Log::WriteFmtArgs(LOGLEVEL level, ConsoleColors color, fmt::string_view fmt, fmt::format_args args)
{
if (level > s_max_level)
return;
fmt::memory_buffer buffer;
fmt::vformat_to(std::back_inserter(buffer), fmt, args);
ExecuteCallbacks(level, color, std::string_view(buffer.data(), buffer.size()));
}

199
common/Console.h Normal file
View File

@@ -0,0 +1,199 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include "fmt/base.h"
#include <cstdarg>
#include <string>
// TODO: This whole thing needs to get ripped out.
enum ConsoleColors
{
Color_Default = 0,
Color_Black,
Color_Green,
Color_Red,
Color_Blue,
Color_Magenta,
Color_Orange,
Color_Gray,
Color_Cyan, // faint visibility, intended for logging PS2/IOP output
Color_Yellow, // faint visibility, intended for logging PS2/IOP output
Color_White, // faint visibility, intended for logging PS2/IOP output
// Strong text *may* result in mis-aligned text in the console, depending on the
// font and the platform, so use these with caution.
Color_StrongBlack,
Color_StrongRed, // intended for errors
Color_StrongGreen, // intended for infrequent state information
Color_StrongBlue, // intended for block headings
Color_StrongMagenta,
Color_StrongOrange, // intended for warnings
Color_StrongGray,
Color_StrongCyan,
Color_StrongYellow,
Color_StrongWhite,
ConsoleColors_Count
};
enum LOGLEVEL
{
LOGLEVEL_NONE, // Silences all log traffic
LOGLEVEL_ERROR,
LOGLEVEL_WARNING,
LOGLEVEL_INFO,
LOGLEVEL_DEV,
LOGLEVEL_DEBUG,
LOGLEVEL_TRACE,
LOGLEVEL_COUNT,
};
// TODO: Move this elsewhere, add channels.
namespace Log
{
// log message callback type
using HostCallbackType = void (*)(LOGLEVEL level, ConsoleColors color, std::string_view message);
// returns the time in seconds since the start of the process
float GetCurrentMessageTime();
// adds a standard console output
bool IsConsoleOutputEnabled();
void SetConsoleOutputLevel(LOGLEVEL level);
// adds a debug console output
bool IsDebugOutputAvailable();
bool IsDebugOutputEnabled();
void SetDebugOutputLevel(LOGLEVEL level);
// adds a file output
bool IsFileOutputEnabled();
bool SetFileOutputLevel(LOGLEVEL level, std::string path);
// returns the log file, this is really dangerous to use if it changes...
std::FILE* GetFileLogHandle();
// adds host output
bool IsHostOutputEnabled();
void SetHostOutputLevel(LOGLEVEL level, HostCallbackType callback);
// sets logging timestamps
bool AreTimestampsEnabled();
void SetTimestampsEnabled(bool enabled);
// Returns the current global filtering level.
LOGLEVEL GetMaxLevel();
// writes a message to the log
void Write(LOGLEVEL level, ConsoleColors color, std::string_view message);
void Writef(LOGLEVEL level, ConsoleColors color, const char* format, ...);
void Writev(LOGLEVEL level, ConsoleColors color, const char* format, va_list ap);
void WriteFmtArgs(LOGLEVEL level, ConsoleColors color, fmt::string_view fmt, fmt::format_args args);
template <typename... T>
__fi static void Write(LOGLEVEL level, ConsoleColors color, fmt::format_string<T...> fmt, T&&... args)
{
// Avoid arg packing if filtered.
if (level <= GetMaxLevel())
return WriteFmtArgs(level, color, fmt, fmt::make_format_args(args...));
}
} // namespace Log
// Adapter classes to handle old code.
template <LOGLEVEL level>
struct ConsoleLogWriter
{
__fi static void Error(std::string_view str) { Log::Write(level, Color_StrongRed, str); }
__fi static void Warning(std::string_view str) { Log::Write(level, Color_StrongOrange, str); }
__fi static void WriteLn(std::string_view str) { Log::Write(level, Color_Default, str); }
__fi static void WriteLn(ConsoleColors color, std::string_view str) { Log::Write(level, color, str); }
__fi static void WriteLn() { Log::Write(level, Color_Default, std::string_view()); }
__fi static void FormatV(const char* format, va_list ap) { Log::Writev(level, Color_Default, format, ap); }
__fi static void FormatV(ConsoleColors color, const char* format, va_list ap) { Log::Writev(level, color, format, ap); }
#define MAKE_PRINTF_CONSOLE_WRITER(color) \
do \
{ \
std::va_list ap; \
va_start(ap, format); \
Log::Writev(level, color, format, ap); \
va_end(ap); \
} while (0)
// clang-format off
static void Error(const char* format, ...) { MAKE_PRINTF_CONSOLE_WRITER(Color_StrongRed); }
static void Warning(const char* format, ...) { MAKE_PRINTF_CONSOLE_WRITER(Color_StrongOrange); }
static void WriteLn(const char* format, ...) { MAKE_PRINTF_CONSOLE_WRITER(Color_Default); }
static void WriteLn(ConsoleColors color, const char* format, ...) { MAKE_PRINTF_CONSOLE_WRITER(color); }
// clang-format on
#undef MAKE_PRINTF_CONSOLE_WRITER
#define MAKE_FMT_CONSOLE_WRITER(color) do \
{ \
if (level <= Log::GetMaxLevel()) \
Log::WriteFmtArgs(level, color, fmt, fmt::make_format_args(args...)); \
} \
while (0)
// clang-format off
template<typename... T> __fi static void ErrorFmt(fmt::format_string<T...> fmt, T&&... args) { MAKE_FMT_CONSOLE_WRITER(Color_StrongRed); }
template<typename... T> __fi static void WarningFmt(fmt::format_string<T...> fmt, T&&... args) { MAKE_FMT_CONSOLE_WRITER(Color_StrongOrange); }
template<typename... T> __fi static void WriteLnFmt(fmt::format_string<T...> fmt, T&&... args) { MAKE_FMT_CONSOLE_WRITER(Color_Default); }
template<typename... T> __fi static void WriteLnFmt(ConsoleColors color, fmt::format_string<T...> fmt, T&&... args) { MAKE_FMT_CONSOLE_WRITER(color); }
// clang-format on
#undef MAKE_FMT_CONSOLE_WRITER
};
struct NullLogWriter
{
// clang-format off
__fi static bool Error(std::string_view str) { return false; }
__fi static bool Warning(std::string_view str) { return false; }
__fi static bool WriteLn(std::string_view str) { return false; }
__fi static bool WriteLn(ConsoleColors color, std::string_view str) { return false; }
__fi static bool WriteLn() { return false; }
__fi static bool Error(const char* format, ...) { return false; }
__fi static bool Warning(const char* format, ...) { return false; }
__fi static bool WriteLn(const char* format, ...) { return false; }
__fi static bool WriteLn(ConsoleColors color, const char* format, ...) { return false; }
template<typename... T> __fi static bool ErrorFmt(fmt::format_string<T...> fmt, T&&... args) { return false; }
template<typename... T> __fi static bool WarningFmt(fmt::format_string<T...> fmt, T&&... args) { return false; }
template<typename... T> __fi static bool WriteLnFmt(fmt::format_string<T...> fmt, T&&... args) { return false; }
template<typename... T> __fi static bool WriteLnFmt(ConsoleColors color, fmt::format_string<T...> fmt, T&&... args) { return false; }
// clang-format on
};
extern ConsoleLogWriter<LOGLEVEL_INFO> Console;
extern ConsoleLogWriter<LOGLEVEL_DEV> DevCon;
#define ERROR_LOG(...) Log::Write(LOGLEVEL_ERROR, Color_StrongRed, __VA_ARGS__)
#define WARNING_LOG(...) Log::Write(LOGLEVEL_WARNING, Color_StrongOrange, __VA_ARGS__)
#define INFO_LOG(...) Log::Write(LOGLEVEL_INFO, Color_White, __VA_ARGS__)
#define DEV_LOG(...) Log::Write(LOGLEVEL_DEV, Color_StrongGray, __VA_ARGS__)
#ifdef _DEBUG
extern ConsoleLogWriter<LOGLEVEL_DEBUG> DbgConWriter;
#define DbgCon DbgConWriter
#define DEBUG_LOG(...) Log::Write(LOGLEVEL_TRACE, Color_Gray, __VA_ARGS__)
#define TRACE_LOG(...) Log::Write(LOGLEVEL_TRACE, Color_Blue, __VA_ARGS__)
#else
extern NullLogWriter DbgConWriter;
#define DbgCon 0 && DbgConWriter
#define DEBUG_LOG(...) (void)0
#define TRACE_LOG(...) (void)0
#endif

380
common/CrashHandler.cpp Normal file
View File

@@ -0,0 +1,380 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Pcsx2Defs.h"
#include "CrashHandler.h"
#include "DynamicLibrary.h"
#include "FileSystem.h"
#include "StringUtil.h"
#include <cinttypes>
#include <cstdio>
#include <ctime>
#if defined(_WIN32)
#include "RedtapeWindows.h"
#include "StackWalker.h"
#include <DbgHelp.h>
class CrashHandlerStackWalker : public StackWalker
{
public:
explicit CrashHandlerStackWalker(HANDLE out_file);
~CrashHandlerStackWalker();
protected:
void OnOutput(LPCSTR szText) override;
private:
HANDLE m_out_file;
};
CrashHandlerStackWalker::CrashHandlerStackWalker(HANDLE out_file)
: StackWalker(RetrieveVerbose, nullptr, GetCurrentProcessId(), GetCurrentProcess())
, m_out_file(out_file)
{
}
CrashHandlerStackWalker::~CrashHandlerStackWalker() = default;
void CrashHandlerStackWalker::OnOutput(LPCSTR szText)
{
if (m_out_file)
{
DWORD written;
WriteFile(m_out_file, szText, static_cast<DWORD>(std::strlen(szText)), &written, nullptr);
}
OutputDebugStringA(szText);
}
static bool WriteMinidump(HMODULE hDbgHelp, HANDLE hFile, HANDLE hProcess, DWORD process_id, DWORD thread_id,
PEXCEPTION_POINTERS exception, MINIDUMP_TYPE type)
{
using PFNMINIDUMPWRITEDUMP =
BOOL(WINAPI*)(HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType,
PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
PMINIDUMP_CALLBACK_INFORMATION CallbackParam);
PFNMINIDUMPWRITEDUMP minidump_write_dump =
hDbgHelp ? reinterpret_cast<PFNMINIDUMPWRITEDUMP>(GetProcAddress(hDbgHelp, "MiniDumpWriteDump")) : nullptr;
if (!minidump_write_dump)
return false;
MINIDUMP_EXCEPTION_INFORMATION mei = {};
if (exception)
{
mei.ThreadId = thread_id;
mei.ExceptionPointers = exception;
mei.ClientPointers = FALSE;
return minidump_write_dump(hProcess, process_id, hFile, type, &mei, nullptr, nullptr);
}
__try
{
RaiseException(EXCEPTION_INVALID_HANDLE, 0, 0, nullptr);
}
__except (WriteMinidump(hDbgHelp, hFile, GetCurrentProcess(), GetCurrentProcessId(), GetCurrentThreadId(),
GetExceptionInformation(), type),
EXCEPTION_EXECUTE_HANDLER)
{
}
return true;
}
static std::wstring s_write_directory;
static DynamicLibrary s_dbghelp_module;
static bool s_in_crash_handler = false;
static void GenerateCrashFilename(wchar_t* buf, size_t len, const wchar_t* prefix, const wchar_t* extension)
{
SYSTEMTIME st = {};
GetLocalTime(&st);
_snwprintf_s(buf, len, _TRUNCATE, L"%s%scrash-%04u-%02u-%02u-%02u-%02u-%02u-%03u.%s",
prefix ? prefix : L"", prefix ? L"\\" : L"",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension);
}
static void WriteMinidumpAndCallstack(PEXCEPTION_POINTERS exi)
{
s_in_crash_handler = true;
wchar_t filename[1024] = {};
GenerateCrashFilename(filename, std::size(filename), s_write_directory.empty() ? nullptr : s_write_directory.c_str(), L"txt");
// might fail
HANDLE hFile = CreateFileW(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr);
if (exi && hFile != INVALID_HANDLE_VALUE)
{
char line[1024];
DWORD written;
std::snprintf(line, std::size(line), "Exception 0x%08X at 0x%p\n", static_cast<unsigned>(exi->ExceptionRecord->ExceptionCode),
exi->ExceptionRecord->ExceptionAddress);
WriteFile(hFile, line, static_cast<DWORD>(std::strlen(line)), &written, nullptr);
}
GenerateCrashFilename(filename, std::size(filename), s_write_directory.empty() ? nullptr : s_write_directory.c_str(), L"dmp");
const MINIDUMP_TYPE minidump_type =
static_cast<MINIDUMP_TYPE>(MiniDumpNormal | MiniDumpWithHandleData | MiniDumpWithProcessThreadData |
MiniDumpWithThreadInfo | MiniDumpWithIndirectlyReferencedMemory);
const HANDLE hMinidumpFile = CreateFileW(filename, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr);
if (hMinidumpFile == INVALID_HANDLE_VALUE ||
!WriteMinidump(static_cast<HMODULE>(s_dbghelp_module.GetHandle()), hMinidumpFile, GetCurrentProcess(), GetCurrentProcessId(),
GetCurrentThreadId(), exi, minidump_type))
{
static const char error_message[] = "Failed to write minidump file.\n";
if (hFile != INVALID_HANDLE_VALUE)
{
DWORD written;
WriteFile(hFile, error_message, sizeof(error_message) - 1, &written, nullptr);
}
}
if (hMinidumpFile != INVALID_HANDLE_VALUE)
CloseHandle(hMinidumpFile);
CrashHandlerStackWalker sw(hFile);
sw.ShowCallstack(GetCurrentThread(), exi ? exi->ContextRecord : nullptr);
if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
}
static LONG NTAPI ExceptionHandler(PEXCEPTION_POINTERS exi)
{
// if the debugger is attached, or we're recursively crashing, let it take care of it.
if (!s_in_crash_handler)
WriteMinidumpAndCallstack(exi);
// returning EXCEPTION_CONTINUE_SEARCH makes sense, except for the fact that it seems to leave zombie processes
// around. instead, force ourselves to terminate.
TerminateProcess(GetCurrentProcess(), 0xFEFEFEFEu);
return EXCEPTION_CONTINUE_SEARCH;
}
bool CrashHandler::Install()
{
// load dbghelp at install/startup, that way we're not LoadLibrary()'ing after a crash
// .. because that probably wouldn't go down well.
HMODULE mod = StackWalker::LoadDbgHelpLibrary();
if (mod)
s_dbghelp_module.Adopt(mod);
SetUnhandledExceptionFilter(ExceptionHandler);
return true;
}
void CrashHandler::SetWriteDirectory(std::string_view dump_directory)
{
s_write_directory = FileSystem::GetWin32Path(dump_directory);
}
void CrashHandler::WriteDumpForCaller()
{
WriteMinidumpAndCallstack(nullptr);
}
#elif !defined(__APPLE__) && defined(HAS_LIBBACKTRACE)
#include "FileSystem.h"
#include <backtrace.h>
#include <signal.h>
#include <sys/mman.h>
#include <unistd.h>
#include <cstdarg>
#include <cstdio>
#include <mutex>
namespace CrashHandler
{
struct BacktraceBuffer
{
char* buffer;
size_t used;
size_t size;
};
static const char* GetSignalName(int signal_no);
static void AllocateBuffer(BacktraceBuffer* buf);
static void FreeBuffer(BacktraceBuffer* buf);
static void AppendToBuffer(BacktraceBuffer* buf, const char* format, ...);
static int BacktraceFullCallback(void* data, uintptr_t pc, const char* filename, int lineno, const char* function);
static void LogCallstack(int signal, const void* exception_pc);
static std::recursive_mutex s_crash_mutex;
static bool s_in_signal_handler = false;
static backtrace_state* s_backtrace_state = nullptr;
} // namespace CrashHandler
const char* CrashHandler::GetSignalName(int signal_no)
{
switch (signal_no)
{
// Don't need to list all of them, there's only a couple we register.
// clang-format off
case SIGSEGV: return "SIGSEGV";
case SIGBUS: return "SIGBUS";
default: return "UNKNOWN";
// clang-format on
}
}
void CrashHandler::AllocateBuffer(BacktraceBuffer* buf)
{
buf->used = 0;
buf->size = __pagesize;
buf->buffer = static_cast<char*>(mmap(nullptr, buf->size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
if (buf->buffer == static_cast<char*>(MAP_FAILED))
{
buf->buffer = nullptr;
buf->size = 0;
}
}
void CrashHandler::FreeBuffer(BacktraceBuffer* buf)
{
if (buf->buffer)
munmap(buf->buffer, buf->size);
}
void CrashHandler::AppendToBuffer(BacktraceBuffer* buf, const char* format, ...)
{
std::va_list ap;
va_start(ap, format);
// Hope this doesn't allocate memory... it *can*, but hopefully unlikely since
// it won't be the first call, and we're providing the buffer.
if (buf->size > 0 && buf->used < (buf->size - 1))
{
const int written = std::vsnprintf(buf->buffer + buf->used, buf->size - buf->used, format, ap);
if (written > 0)
buf->used += static_cast<size_t>(written);
}
va_end(ap);
}
int CrashHandler::BacktraceFullCallback(void* data, uintptr_t pc, const char* filename, int lineno,
const char* function)
{
BacktraceBuffer* buf = static_cast<BacktraceBuffer*>(data);
AppendToBuffer(buf, " %016p", pc);
if (function)
AppendToBuffer(buf, " %s", function);
if (filename)
AppendToBuffer(buf, " [%s:%d]", filename, lineno);
AppendToBuffer(buf, "\n");
return 0;
}
void CrashHandler::LogCallstack(int signal, const void* exception_pc)
{
BacktraceBuffer buf;
AllocateBuffer(&buf);
if (signal != 0 || exception_pc)
AppendToBuffer(&buf, "*************** Unhandled %s at %p ***************\n", GetSignalName(signal), exception_pc);
else
AppendToBuffer(&buf, "*******************************************************************\n");
const int rc = backtrace_full(s_backtrace_state, 0, BacktraceFullCallback, nullptr, &buf);
if (rc != 0)
AppendToBuffer(&buf, " backtrace_full() failed: %d\n");
AppendToBuffer(&buf, "*******************************************************************\n");
if (buf.used > 0)
write(STDERR_FILENO, buf.buffer, buf.used);
FreeBuffer(&buf);
}
void CrashHandler::CrashSignalHandler(int signal, siginfo_t* siginfo, void* ctx)
{
std::unique_lock lock(s_crash_mutex);
// If we crash somewhere in libbacktrace, don't bother trying again.
if (!s_in_signal_handler)
{
s_in_signal_handler = true;
#if defined(__APPLE__) && defined(__x86_64__)
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__ss.__rip);
#elif defined(__FreeBSD__) && defined(__x86_64__)
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.mc_rip);
#elif defined(__x86_64__)
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext.gregs[REG_RIP]);
#else
void* const exception_pc = nullptr;
#endif
LogCallstack(signal, exception_pc);
s_in_signal_handler = false;
}
lock.unlock();
// We can't continue from here. Just bail out and dump core.
std::fputs("Aborting application.\n", stderr);
std::fflush(stderr);
std::abort();
}
bool CrashHandler::Install()
{
const std::string progpath = FileSystem::GetProgramPath();
s_backtrace_state = backtrace_create_state(progpath.empty() ? nullptr : progpath.c_str(), 0, nullptr, nullptr);
if (!s_backtrace_state)
return false;
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO | SA_NODEFER;
sa.sa_sigaction = CrashSignalHandler;
if (sigaction(SIGBUS, &sa, nullptr) != 0)
return false;
if (sigaction(SIGSEGV, &sa, nullptr) != 0)
return false;
return true;
}
void CrashHandler::SetWriteDirectory(std::string_view dump_directory)
{
}
void CrashHandler::WriteDumpForCaller()
{
LogCallstack(0, nullptr);
}
#else
bool CrashHandler::Install()
{
return false;
}
void CrashHandler::SetWriteDirectory(std::string_view dump_directory)
{
}
void CrashHandler::WriteDumpForCaller()
{
}
void CrashHandler::CrashSignalHandler(int signal, siginfo_t* siginfo, void* ctx)
{
// We can't continue from here. Just bail out and dump core.
std::fputs("Aborting application.\n", stderr);
std::fflush(stderr);
std::abort();
}
#endif

20
common/CrashHandler.h Normal file
View File

@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <string_view>
#ifndef _WIN32
#include <csignal>
#endif
namespace CrashHandler
{
bool Install();
void SetWriteDirectory(std::string_view dump_directory);
void WriteDumpForCaller();
#ifndef _WIN32
// Allow crash handler to be invoked from a signal.
void CrashSignalHandler(int signal, siginfo_t* siginfo, void* ctx);
#endif
} // namespace CrashHandler

View File

@@ -0,0 +1,617 @@
// 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/Darwin/DarwinMisc.h"
#include "common/Error.h"
#include "common/Pcsx2Types.h"
#include "common/Threading.h"
#include "common/WindowInfo.h"
#include "common/HostSys.h"
#include <csignal>
#include <cstring>
#include <cstdlib>
#include <optional>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <time.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/mach_time.h>
#include <mach/mach_vm.h>
#include <mach/task.h>
#include <mach/vm_map.h>
#include <mutex>
#include <ApplicationServices/ApplicationServices.h>
#include <IOKit/pwr_mgt/IOPMLib.h>
// Darwin (OSX) is a bit different from Linux when requesting properties of
// the OS because of its BSD/Mach heritage. Helpfully, most of this code
// should translate pretty well to other *BSD systems. (e.g.: the sysctl(3)
// interface).
//
// For an overview of all of Darwin's sysctls, check:
// https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/sysctl.3.html
// Return the total physical memory on the machine, in bytes. Returns 0 on
// failure (not supported by the operating system).
u64 GetPhysicalMemory()
{
u64 getmem = 0;
size_t len = sizeof(getmem);
int mib[] = {CTL_HW, HW_MEMSIZE};
if (sysctl(mib, std::size(mib), &getmem, &len, NULL, 0) < 0)
perror("sysctl:");
return getmem;
}
u64 GetAvailablePhysicalMemory()
{
const mach_port_t host_port = mach_host_self();
vm_size_t page_size;
// Get the system's page size.
if (host_page_size(host_port, &page_size) != KERN_SUCCESS)
return 0;
vm_statistics64_data_t vm_stat;
mach_msg_type_number_t host_size = sizeof(vm_statistics64_data_t) / sizeof(integer_t);
// Get system memory statistics.
if (host_statistics64(host_port, HOST_VM_INFO, reinterpret_cast<host_info64_t>(&vm_stat), &host_size) != KERN_SUCCESS)
return 0;
// Get the number of free and inactive pages.
const u64 free_pages = static_cast<u64>(vm_stat.free_count);
const u64 inactive_pages = static_cast<u64>(vm_stat.inactive_count);
// Calculate available memory.
const u64 get_available_mem = (free_pages + inactive_pages) * page_size;
return get_available_mem;
}
static mach_timebase_info_data_t s_timebase_info;
static const u64 tickfreq = []() {
if (mach_timebase_info(&s_timebase_info) != KERN_SUCCESS)
abort();
return (u64)1e9 * (u64)s_timebase_info.denom / (u64)s_timebase_info.numer;
}();
// returns the performance-counter frequency: ticks per second (Hz)
//
// usage:
// u64 seconds_passed = GetCPUTicks() / GetTickFrequency();
// u64 millis_passed = (GetCPUTicks() * 1000) / GetTickFrequency();
//
// NOTE: multiply, subtract, ... your ticks before dividing by
// GetTickFrequency() to maintain good precision.
u64 GetTickFrequency()
{
return tickfreq;
}
// return the number of "ticks" since some arbitrary, fixed time in the
// past. On OSX x86(-64), this is actually the number of nanoseconds passed,
// because mach_timebase_info.numer == denom == 1. So "ticks" ==
// nanoseconds.
u64 GetCPUTicks()
{
return mach_absolute_time();
}
static std::string sysctl_str(int category, int name)
{
char buf[32];
size_t len = sizeof(buf);
int mib[] = {category, name};
sysctl(mib, std::size(mib), buf, &len, nullptr, 0);
return std::string(buf, len > 0 ? len - 1 : 0);
}
template <typename T>
static std::optional<T> sysctlbyname_T(const char* name)
{
T output = 0;
size_t output_size = sizeof(output);
if (sysctlbyname(name, &output, &output_size, nullptr, 0) != 0)
return std::nullopt;
if (output_size != sizeof(output))
{
ERROR_LOG("(DarwinMisc) sysctl {} gave unexpected size {}", name, output_size);
return std::nullopt;
}
return output;
}
std::string GetOSVersionString()
{
std::string type = sysctl_str(CTL_KERN, KERN_OSTYPE);
std::string release = sysctl_str(CTL_KERN, KERN_OSRELEASE);
std::string arch = sysctl_str(CTL_HW, HW_MACHINE);
return type + " " + release + " " + arch;
}
static IOPMAssertionID s_pm_assertion;
bool Common::InhibitScreensaver(bool inhibit)
{
if (s_pm_assertion)
{
IOPMAssertionRelease(s_pm_assertion);
s_pm_assertion = 0;
}
if (inhibit)
IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, CFSTR("Playing a game"), &s_pm_assertion);
return true;
}
void Common::SetMousePosition(int x, int y)
{
// Little bit ugly but;
// Creating mouse move events and posting them wasn't very reliable.
// Calling CGWarpMouseCursorPosition without CGAssociateMouseAndMouseCursorPosition(false)
// ends up with the cursor feeling "sticky".
CGAssociateMouseAndMouseCursorPosition(false);
CGWarpMouseCursorPosition(CGPointMake(x, y));
CGAssociateMouseAndMouseCursorPosition(true); // The default state
return;
}
CFMachPortRef mouseEventTap = nullptr;
CFRunLoopSourceRef mouseRunLoopSource = nullptr;
static std::function<void(int, int)> fnMouseMoveCb;
CGEventRef mouseMoveCallback(CGEventTapProxy, CGEventType type, CGEventRef event, void* arg)
{
if (type == kCGEventMouseMoved)
{
const CGPoint location = CGEventGetLocation(event);
fnMouseMoveCb(location.x, location.y);
}
return event;
}
bool Common::AttachMousePositionCb(std::function<void(int, int)> cb)
{
if (!AXIsProcessTrusted())
{
Console.Warning("Process isn't trusted with accessibility permissions. Mouse tracking will not work!");
}
fnMouseMoveCb = cb;
mouseEventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
CGEventMaskBit(kCGEventMouseMoved), mouseMoveCallback, nullptr);
if (!mouseEventTap)
{
Console.Warning("Unable to create mouse moved event tap. Mouse tracking will not work!");
return false;
}
mouseRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mouseEventTap, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), mouseRunLoopSource, kCFRunLoopCommonModes);
return true;
}
void Common::DetachMousePositionCb()
{
if (mouseRunLoopSource)
{
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), mouseRunLoopSource, kCFRunLoopCommonModes);
CFRelease(mouseRunLoopSource);
}
if (mouseEventTap)
{
CFRelease(mouseEventTap);
}
mouseRunLoopSource = nullptr;
mouseEventTap = nullptr;
}
void Threading::Sleep(int ms)
{
usleep(1000 * ms);
}
void Threading::SleepUntil(u64 ticks)
{
// This is definitely sub-optimal, but apparently clock_nanosleep() doesn't exist.
const s64 diff = static_cast<s64>(ticks - GetCPUTicks());
if (diff <= 0)
return;
const u64 nanos = (static_cast<u64>(diff) * static_cast<u64>(s_timebase_info.denom)) / static_cast<u64>(s_timebase_info.numer);
if (nanos == 0)
return;
struct timespec ts;
ts.tv_sec = nanos / 1000000000ULL;
ts.tv_nsec = nanos % 1000000000ULL;
nanosleep(&ts, nullptr);
}
std::vector<DarwinMisc::CPUClass> DarwinMisc::GetCPUClasses()
{
std::vector<CPUClass> out;
if (std::optional<u32> nperflevels = sysctlbyname_T<u32>("hw.nperflevels"))
{
char name[64];
for (u32 i = 0; i < *nperflevels; i++)
{
snprintf(name, sizeof(name), "hw.perflevel%u.physicalcpu", i);
std::optional<u32> physicalcpu = sysctlbyname_T<u32>(name);
snprintf(name, sizeof(name), "hw.perflevel%u.logicalcpu", i);
std::optional<u32> logicalcpu = sysctlbyname_T<u32>(name);
char levelname[64];
size_t levelname_size = sizeof(levelname);
snprintf(name, sizeof(name), "hw.perflevel%u.name", i);
if (0 != sysctlbyname(name, levelname, &levelname_size, nullptr, 0))
strcpy(levelname, "???");
if (!physicalcpu.has_value() || !logicalcpu.has_value())
{
Console.Warning("(DarwinMisc) Perf level %u is missing data on %s cpus!",
i, !physicalcpu.has_value() ? "physical" : "logical");
continue;
}
out.push_back({levelname, *physicalcpu, *logicalcpu});
}
}
else if (std::optional<u32> physcpu = sysctlbyname_T<u32>("hw.physicalcpu"))
{
out.push_back({"Default", *physcpu, sysctlbyname_T<u32>("hw.logicalcpu").value_or(0)});
}
else
{
Console.Warning("(DarwinMisc) Couldn't get cpu core count!");
}
return out;
}
size_t HostSys::GetRuntimePageSize()
{
return sysctlbyname_T<u32>("hw.pagesize").value_or(0);
}
size_t HostSys::GetRuntimeCacheLineSize()
{
return static_cast<size_t>(std::max<s64>(sysctlbyname_T<s64>("hw.cachelinesize").value_or(0), 0));
}
static __ri vm_prot_t MachProt(const PageProtectionMode& mode)
{
vm_prot_t machmode = (mode.CanWrite()) ? VM_PROT_WRITE : 0;
machmode |= (mode.CanRead()) ? VM_PROT_READ : 0;
machmode |= (mode.CanExecute()) ? (VM_PROT_EXECUTE | VM_PROT_READ) : 0;
return machmode;
}
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;
#ifdef __aarch64__
// We can't allocate executable memory with mach_vm_allocate() on Apple Silicon.
// Instead, we need to use MAP_JIT with mmap(), which does not support fixed mappings.
if (mode.CanExecute())
{
if (base)
return nullptr;
const u32 mmap_prot = mode.CanWrite() ? (PROT_READ | PROT_WRITE | PROT_EXEC) : (PROT_READ | PROT_EXEC);
const u32 flags = MAP_PRIVATE | MAP_ANON | MAP_JIT;
void* const res = mmap(nullptr, size, mmap_prot, flags, -1, 0);
return (res == MAP_FAILED) ? nullptr : res;
}
#endif
kern_return_t ret = mach_vm_allocate(mach_task_self(), reinterpret_cast<mach_vm_address_t*>(&base), size,
base ? VM_FLAGS_FIXED : VM_FLAGS_ANYWHERE);
if (ret != KERN_SUCCESS)
{
DEV_LOG("mach_vm_allocate() returned {}", ret);
return nullptr;
}
ret = mach_vm_protect(mach_task_self(), reinterpret_cast<mach_vm_address_t>(base), size, false, MachProt(mode));
if (ret != KERN_SUCCESS)
{
DEV_LOG("mach_vm_protect() returned {}", ret);
mach_vm_deallocate(mach_task_self(), reinterpret_cast<mach_vm_address_t>(base), size);
return nullptr;
}
return base;
}
void HostSys::Munmap(void* base, size_t size)
{
if (!base)
return;
mach_vm_deallocate(mach_task_self(), reinterpret_cast<mach_vm_address_t>(base), size);
}
void HostSys::MemProtect(void* baseaddr, size_t size, const PageProtectionMode& mode)
{
pxAssertMsg((size & (__pagesize - 1)) == 0, "Size is page aligned");
kern_return_t res = mach_vm_protect(mach_task_self(), reinterpret_cast<mach_vm_address_t>(baseaddr), size, false,
MachProt(mode));
if (res != KERN_SUCCESS) [[unlikely]]
{
ERROR_LOG("mach_vm_protect() failed: {}", res);
pxFailRel("mach_vm_protect() failed");
}
}
std::string HostSys::GetFileMappingName(const char* prefix)
{
// name actually is not used.
return {};
}
void* HostSys::CreateSharedMemory(const char* name, size_t size)
{
mach_vm_size_t vm_size = size;
mach_port_t port;
const kern_return_t res = mach_make_memory_entry_64(
mach_task_self(), &vm_size, 0, MAP_MEM_NAMED_CREATE | VM_PROT_READ | VM_PROT_WRITE, &port, MACH_PORT_NULL);
if (res != KERN_SUCCESS)
{
ERROR_LOG("mach_make_memory_entry_64() failed: {}", res);
return nullptr;
}
return reinterpret_cast<void*>(static_cast<uintptr_t>(port));
}
void HostSys::DestroySharedMemory(void* ptr)
{
mach_port_deallocate(mach_task_self(), static_cast<mach_port_t>(reinterpret_cast<uintptr_t>(ptr)));
}
void* HostSys::MapSharedMemory(void* handle, size_t offset, void* baseaddr, size_t size, const PageProtectionMode& mode)
{
mach_vm_address_t ptr = reinterpret_cast<mach_vm_address_t>(baseaddr);
const kern_return_t res = mach_vm_map(mach_task_self(), &ptr, size, 0, baseaddr ? VM_FLAGS_FIXED : VM_FLAGS_ANYWHERE,
static_cast<mach_port_t>(reinterpret_cast<uintptr_t>(handle)), offset, FALSE,
MachProt(mode), VM_PROT_READ | VM_PROT_WRITE, VM_INHERIT_NONE);
if (res != KERN_SUCCESS)
{
ERROR_LOG("mach_vm_map() failed: {}", res);
return nullptr;
}
return reinterpret_cast<void*>(ptr);
}
void HostSys::UnmapSharedMemory(void* baseaddr, size_t size)
{
const kern_return_t res = mach_vm_deallocate(mach_task_self(), reinterpret_cast<mach_vm_address_t>(baseaddr), size);
if (res != KERN_SUCCESS)
pxFailRel("Failed to unmap shared memory");
}
#ifdef _M_ARM64
void HostSys::FlushInstructionCache(void* address, u32 size)
{
__builtin___clear_cache(reinterpret_cast<char*>(address), reinterpret_cast<char*>(address) + size);
}
#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 (mach_vm_deallocate(mach_task_self(), reinterpret_cast<mach_vm_address_t>(m_base_ptr), m_size) != KERN_SUCCESS)
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");
mach_vm_address_t alloc = 0;
const kern_return_t res =
mach_vm_map(mach_task_self(), &alloc, size, 0, VM_FLAGS_ANYWHERE,
MEMORY_OBJECT_NULL, 0, false, VM_PROT_NONE, VM_PROT_NONE, VM_INHERIT_NONE);
if (res != KERN_SUCCESS)
{
ERROR_LOG("mach_vm_map() failed: {}", res);
return {};
}
return std::unique_ptr<SharedMemoryMappingArea>(new SharedMemoryMappingArea(reinterpret_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));
const kern_return_t res =
mach_vm_map(mach_task_self(), reinterpret_cast<mach_vm_address_t*>(&map_base), map_size, 0, VM_FLAGS_OVERWRITE,
static_cast<mach_port_t>(reinterpret_cast<uintptr_t>(file_handle)), file_offset, false,
MachProt(mode), VM_PROT_READ | VM_PROT_WRITE, VM_INHERIT_NONE);
if (res != KERN_SUCCESS) [[unlikely]]
{
ERROR_LOG("mach_vm_map() failed: {}", res);
return nullptr;
}
m_num_mappings++;
return static_cast<u8*>(map_base);
}
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));
const kern_return_t res =
mach_vm_map(mach_task_self(), reinterpret_cast<mach_vm_address_t*>(&map_base), map_size, 0, VM_FLAGS_OVERWRITE,
MEMORY_OBJECT_NULL, 0, false, VM_PROT_NONE, VM_PROT_NONE, VM_INHERIT_NONE);
if (res != KERN_SUCCESS) [[unlikely]]
{
ERROR_LOG("mach_vm_map() failed: {}", res);
return false;
}
m_num_mappings--;
return true;
}
#ifdef _M_ARM64
static thread_local int s_code_write_depth = 0;
void HostSys::BeginCodeWrite()
{
if ((s_code_write_depth++) == 0)
pthread_jit_write_protect_np(0);
}
void HostSys::EndCodeWrite()
{
pxAssert(s_code_write_depth > 0);
if ((--s_code_write_depth) == 0)
pthread_jit_write_protect_np(1);
}
[[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);
static std::recursive_mutex s_exception_handler_mutex;
static bool s_in_exception_handler = false;
static bool s_installed = false;
} // namespace PageFaultHandler
void PageFaultHandler::SignalHandler(int sig, siginfo_t* info, void* ctx)
{
#if defined(_M_X86)
void* const exception_address =
reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__es.__faultvaddr);
void* const exception_pc = reinterpret_cast<void*>(static_cast<ucontext_t*>(ctx)->uc_mcontext->__ss.__rip);
const bool is_write = (static_cast<ucontext_t*>(ctx)->uc_mcontext->__es.__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
// 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.sa_sigaction = SignalHandler;
// MacOS uses SIGBUS for memory permission violations, as well as SIGSEGV on ARM64.
if (sigaction(SIGBUS, &sa, nullptr) != 0)
{
Error::SetErrno(error, "sigaction() for SIGBUS failed: ", errno);
return false;
}
#ifdef _M_ARM64
if (sigaction(SIGSEGV, &sa, nullptr) != 0)
{
Error::SetErrno(error, "sigaction() for SIGSEGV failed: ", errno);
return false;
}
#endif
// Allow us to ignore faults when running under lldb.
task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, MACH_PORT_NULL, EXCEPTION_DEFAULT, 0);
s_installed = true;
return true;
}

View File

@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#ifdef __APPLE__
#include <string>
#include <vector>
#include "common/Pcsx2Types.h"
namespace DarwinMisc {
struct CPUClass {
std::string name;
u32 num_physical;
u32 num_logical;
};
std::vector<CPUClass> GetCPUClasses();
}
#endif

View File

@@ -0,0 +1,279 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/Threading.h"
#include "common/Assertions.h"
#include <cstdio>
#include <cassert> // assert
#include <sched.h>
#include <sys/time.h> // gettimeofday()
#include <pthread.h>
#include <unistd.h>
#include <mach/mach.h>
#include <mach/mach_error.h> // mach_error_string()
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/mach_time.h> // mach_absolute_time()
#include <mach/semaphore.h> // semaphore_*()
#include <mach/task.h> // semaphore_create() and semaphore_destroy()
#include <mach/thread_act.h>
// 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()
{
// Darwin has customizable schedulers, see xnu/osfmk/man. Not
// implemented yet though (and not sure if useful for pcsx2).
}
__forceinline void Threading::DisableHiresScheduler()
{
// see EnableHiresScheduler()
}
// Just like on Windows, this is not really the number of ticks per second,
// but just a factor by which one has to divide GetThreadCpuTime() or
// pxThread::GetCpuTime() if one wants to receive a value in seconds. NOTE:
// doing this will of course yield precision loss.
u64 Threading::GetThreadTicksPerSecond()
{
return 1000000; // the *CpuTime() functions return values in microseconds
}
// gets the CPU time used by the current thread (both system and user), in
// microseconds, returns 0 on failure
static u64 getthreadtime(thread_port_t thread)
{
mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
thread_basic_info_data_t info;
kern_return_t kr = thread_info(thread, THREAD_BASIC_INFO,
(thread_info_t)&info, &count);
if (kr != KERN_SUCCESS)
{
return 0;
}
// add system and user time
return (u64)info.user_time.seconds * (u64)1e6 +
(u64)info.user_time.microseconds +
(u64)info.system_time.seconds * (u64)1e6 +
(u64)info.system_time.microseconds;
}
// Returns the current timestamp (not relative to a real world clock) in microseconds
u64 Threading::GetThreadCpuTime()
{
// we could also use mach_thread_self() and mach_port_deallocate(), but
// that calls upon mach traps (kinda like system calls). Unless I missed
// something in the COMMPAGE (like Linux vDSO) which makes overrides it
// to be user-space instead. In contract,
// pthread_mach_thread_np(pthread_self()) is entirely in user-space.
u64 us = getthreadtime(pthread_mach_thread_np(pthread_self()));
return us;
}
// --------------------------------------------------------------------------------------
// Semaphore Implementation for Darwin/OSX
//
// Sadly, Darwin/OSX needs its own implementation of Semaphores instead of
// relying on phtreads, because OSX unnamed semaphore (the best kind)
// support is very poor.
//
// This implementation makes use of Mach primitives instead. These are also
// what Grand Central Dispatch (GCD) is based on, as far as I understand:
// http://newosxbook.com/articles/GCD.html.
//
// --------------------------------------------------------------------------------------
static void MACH_CHECK(kern_return_t mach_retval)
{
if (mach_retval != KERN_SUCCESS)
{
fprintf(stderr, "mach error: %s", mach_error_string(mach_retval));
assert(mach_retval == KERN_SUCCESS);
}
}
Threading::KernelSemaphore::KernelSemaphore()
{
MACH_CHECK(semaphore_create(mach_task_self(), &m_sema, SYNC_POLICY_FIFO, 0));
}
Threading::KernelSemaphore::~KernelSemaphore()
{
MACH_CHECK(semaphore_destroy(mach_task_self(), m_sema));
}
void Threading::KernelSemaphore::Post()
{
MACH_CHECK(semaphore_signal(m_sema));
}
void Threading::KernelSemaphore::Wait()
{
MACH_CHECK(semaphore_wait(m_sema));
}
bool Threading::KernelSemaphore::TryWait()
{
mach_timespec_t time = {};
kern_return_t res = semaphore_timedwait(m_sema, time);
if (res == KERN_OPERATION_TIMED_OUT)
return false;
MACH_CHECK(res);
return true;
}
Threading::ThreadHandle::ThreadHandle() = default;
Threading::ThreadHandle::ThreadHandle(const ThreadHandle& handle)
: m_native_handle(handle.m_native_handle)
{
}
Threading::ThreadHandle::ThreadHandle(ThreadHandle&& handle)
: m_native_handle(handle.m_native_handle)
{
handle.m_native_handle = nullptr;
}
Threading::ThreadHandle::~ThreadHandle() = default;
Threading::ThreadHandle Threading::ThreadHandle::GetForCallingThread()
{
ThreadHandle ret;
ret.m_native_handle = pthread_self();
return ret;
}
Threading::ThreadHandle& Threading::ThreadHandle::operator=(ThreadHandle&& handle)
{
m_native_handle = handle.m_native_handle;
handle.m_native_handle = nullptr;
return *this;
}
Threading::ThreadHandle& Threading::ThreadHandle::operator=(const ThreadHandle& handle)
{
m_native_handle = handle.m_native_handle;
return *this;
}
u64 Threading::ThreadHandle::GetCPUTime() const
{
return getthreadtime(pthread_mach_thread_np((pthread_t)m_native_handle));
}
bool Threading::ThreadHandle::SetAffinity(u64 processor_mask) const
{
// Doesn't appear to be possible to set affinity.
return false;
}
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;
}
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;
}
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;
}
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;
}
// name can be up to 16 bytes
void Threading::SetNameOfCurrentThread(const char* name)
{
pthread_setname_np(name);
}

166
common/DynamicLibrary.cpp Normal file
View File

@@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/DynamicLibrary.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/Error.h"
#include "common/FileSystem.h"
#include "common/SmallString.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include <cstring>
#include "fmt/format.h"
#ifdef _WIN32
#include "common/RedtapeWindows.h"
#else
#include <dlfcn.h>
#ifdef __APPLE__
#include "common/CocoaTools.h"
#endif
#endif
DynamicLibrary::DynamicLibrary() = default;
DynamicLibrary::DynamicLibrary(const char* filename)
{
Error error;
if (!Open(filename, &error))
Console.ErrorFmt("DynamicLibrary open failed: {}", error.GetDescription());
}
DynamicLibrary::DynamicLibrary(DynamicLibrary&& move)
: m_handle(move.m_handle)
{
move.m_handle = nullptr;
}
DynamicLibrary::~DynamicLibrary()
{
Close();
}
std::string DynamicLibrary::GetUnprefixedFilename(const char* filename)
{
#if defined(_WIN32)
return std::string(filename) + ".dll";
#elif defined(__APPLE__)
return std::string(filename) + ".dylib";
#else
return std::string(filename) + ".so";
#endif
}
std::string DynamicLibrary::GetVersionedFilename(const char* libname, int major, int minor)
{
#if defined(_WIN32)
if (major >= 0 && minor >= 0)
return fmt::format("{}-{}-{}.dll", libname, major, minor);
else if (major >= 0)
return fmt::format("{}-{}.dll", libname, major);
else
return fmt::format("{}.dll", libname);
#elif defined(__APPLE__)
const char* prefix = std::strncmp(libname, "lib", 3) ? "lib" : "";
if (major >= 0 && minor >= 0)
return fmt::format("{}{}.{}.{}.dylib", prefix, libname, major, minor);
else if (major >= 0)
return fmt::format("{}{}.{}.dylib", prefix, libname, major);
else
return fmt::format("{}{}.dylib", prefix, libname);
#else
const char* prefix = std::strncmp(libname, "lib", 3) ? "lib" : "";
if (major >= 0 && minor >= 0)
return fmt::format("{}{}.so.{}.{}", prefix, libname, major, minor);
else if (major >= 0)
return fmt::format("{}{}.so.{}", prefix, libname, major);
else
return fmt::format("{}{}.so", prefix, libname);
#endif
}
bool DynamicLibrary::Open(const char* filename, Error* error)
{
#ifdef _WIN32
m_handle = reinterpret_cast<void*>(LoadLibraryW(StringUtil::UTF8StringToWideString(filename).c_str()));
if (!m_handle)
{
Error::SetWin32(error, TinyString::from_format("Loading {} failed: ", filename), GetLastError());
return false;
}
return true;
#else
m_handle = dlopen(filename, RTLD_NOW);
if (!m_handle)
{
#ifdef __APPLE__
// On MacOS, try searching in Frameworks.
if (!Path::IsAbsolute(filename))
{
std::optional<std::string> bundle_path = CocoaTools::GetBundlePath();
if (bundle_path.has_value())
{
std::string frameworks_path = fmt::format("{}/Contents/Frameworks/{}", bundle_path.value(), filename);
if (FileSystem::FileExists(frameworks_path.c_str()))
{
m_handle = dlopen(frameworks_path.c_str(), RTLD_NOW);
if (m_handle)
{
Error::Clear(error);
return true;
}
}
}
}
#endif
const char* err = dlerror();
Error::SetStringFmt(error, "Loading {} failed: {}", filename, err ? err : "<UNKNOWN>");
return false;
}
return true;
#endif
}
void DynamicLibrary::Adopt(void* handle)
{
pxAssertRel(handle, "Handle is valid");
Close();
m_handle = handle;
}
void DynamicLibrary::Close()
{
if (!IsOpen())
return;
#ifdef _WIN32
FreeLibrary(reinterpret_cast<HMODULE>(m_handle));
#else
dlclose(m_handle);
#endif
m_handle = nullptr;
}
void* DynamicLibrary::GetSymbolAddress(const char* name) const
{
#ifdef _WIN32
return reinterpret_cast<void*>(GetProcAddress(reinterpret_cast<HMODULE>(m_handle), name));
#else
return reinterpret_cast<void*>(dlsym(m_handle, name));
#endif
}
DynamicLibrary& DynamicLibrary::operator=(DynamicLibrary&& move)
{
Close();
m_handle = move.m_handle;
move.m_handle = nullptr;
return *this;
}

79
common/DynamicLibrary.h Normal file
View File

@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <string>
class Error;
/**
* Provides a platform-independent interface for loading a dynamic library and retrieving symbols.
* The interface maintains an internal reference count to allow one handle to be shared between
* multiple users.
*/
class DynamicLibrary final
{
public:
/// Default constructor, does not load a library.
DynamicLibrary();
/// Automatically loads the specified library. Call IsOpen() to check validity before use.
DynamicLibrary(const char* filename);
/// Move constructor, transfers ownership.
DynamicLibrary(DynamicLibrary&& move);
/// Closes the library.
~DynamicLibrary();
/// Returns the specified library name with the platform-specific suffix added.
static std::string GetUnprefixedFilename(const char* filename);
/// Returns the specified library name in platform-specific format.
/// Major/minor versions will not be included if set to -1.
/// If libname already contains the "lib" prefix, it will not be added again.
/// Windows: LIBNAME-MAJOR-MINOR.dll
/// Linux: libLIBNAME.so.MAJOR.MINOR
/// Mac: libLIBNAME.MAJOR.MINOR.dylib
static std::string GetVersionedFilename(const char* libname, int major = -1, int minor = -1);
/// Returns true if a module is loaded, otherwise false.
bool IsOpen() const { return m_handle != nullptr; }
/// Loads (or replaces) the handle with the specified library file name.
/// Returns true if the library was loaded and can be used.
bool Open(const char* filename, Error* error);
/// Adopts, or takes ownership of an existing opened library.
void Adopt(void* handle);
/// Unloads the library, any function pointers from this library are no longer valid.
void Close();
/// Returns the address of the specified symbol (function or variable) as an untyped pointer.
/// If the specified symbol does not exist in this library, nullptr is returned.
void* GetSymbolAddress(const char* name) const;
/// Obtains the address of the specified symbol, automatically casting to the correct type.
/// Returns true if the symbol was found and assigned, otherwise false.
template <typename T>
bool GetSymbol(const char* name, T* ptr) const
{
*ptr = reinterpret_cast<T>(GetSymbolAddress(name));
return *ptr != nullptr;
}
/// Returns the opaque OS-specific handle.
void* GetHandle() const { return m_handle; }
/// Move assignment, transfer ownership.
DynamicLibrary& operator=(DynamicLibrary&& move);
private:
DynamicLibrary(const DynamicLibrary&) = delete;
DynamicLibrary& operator=(const DynamicLibrary&) = delete;
/// Platform-dependent data type representing a dynamic library handle.
void* m_handle = nullptr;
};

261
common/Easing.h Normal file
View File

@@ -0,0 +1,261 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <cmath>
// From https://github.com/nicolausYes/easing-functions/blob/master/src/easing.cpp
namespace Easing {
static constexpr float pi = 3.1415926545f;
template<typename T>
__ri static T InSine(T t)
{
return std::sin(1.5707963f * t);
}
template<typename T>
__ri static T OutSine(T t)
{
return 1 + std::sin(1.5707963f * (--t));
}
template<typename T>
__ri static T InOutSine(T t)
{
return 0.5f * (1 + std::sin(3.1415926f * (t - 0.5f)));
}
template<typename T>
__ri static T InQuad(T t)
{
return t * t;
}
template<typename T>
__ri static T OutQuad(T t)
{
return t * (2 - t);
}
template<typename T>
__ri static T InOutQuad(T t)
{
return t < 0.5f ? 2 * t * t : t * (4 - 2 * t) - 1;
}
template<typename T>
__ri static T InCubic(T t)
{
return t * t * t;
}
template<typename T>
__ri static T OutCubic(T t)
{
return 1 + (--t) * t * t;
}
template<typename T>
__ri static T InOutCubic(T t)
{
return t < 0.5f ? 4 * t * t * t : 1 + (--t) * (2 * (--t)) * (2 * t);
}
template<typename T>
__ri static T InQuart(T t)
{
t *= t;
return t * t;
}
template<typename T>
__ri static T OutQuart(T t)
{
t = (--t) * t;
return 1 - t * t;
}
template<typename T>
__ri static T InOutQuart(T t)
{
if (t < 0.5)
{
t *= t;
return 8 * t * t;
}
else
{
t = (--t) * t;
return 1 - 8 * t * t;
}
}
template<typename T>
__ri static T InQuint(T t)
{
T t2 = t * t;
return t * t2 * t2;
}
template<typename T>
__ri static T OutQuint(T t)
{
T t2 = (--t) * t;
return 1 + t * t2 * t2;
}
template<typename T>
__ri static T InOutQuint(T t)
{
T t2;
if (t < 0.5)
{
t2 = t * t;
return 16 * t * t2 * t2;
}
else
{
t2 = (--t) * t;
return 1 + 16 * t * t2 * t2;
}
}
template<typename T>
__ri static T InExpo(T t)
{
return (std::pow(2, 8 * t) - 1) / 255;
}
template<typename T>
__ri static T OutExpo(T t)
{
return 1 - std::pow(2, -8 * t);
}
template<typename T>
__ri static T InOutExpo(T t)
{
if (t < 0.5f)
{
return (std::pow(2, 16 * t) - 1) / 510;
}
else
{
return 1 - 0.5f * std::pow(2, -16 * (t - 0.5f));
}
}
template<typename T>
__ri static T InCirc(T t)
{
return 1 - std::sqrt(1 - t);
}
template<typename T>
__ri static T OutCirc(T t)
{
return std::sqrt(t);
}
template<typename T>
__ri static T InOutCirc(T t)
{
if (t < 0.5f)
{
return (1 - std::sqrt(1 - 2 * t)) * 0.5f;
}
else
{
return (1 + std::sqrt(2 * t - 1)) * 0.5f;
}
}
template<typename T>
__ri static T InBack(T t)
{
return t * t * (2.70158f * t - 1.70158f);
}
template<typename T>
static T OutBack(T t)
{
t -= 1;
return 1 + t * t * (2.70158f * t + 1.70158f);
}
template<typename T>
__ri static T InOutBack(T t)
{
if (t < 0.5f)
{
return t * t * (7 * t - 2.5f) * 2;
}
else
{
return 1 + (--t) * t * 2 * (7 * t + 2.5f);
}
}
template<typename T>
__ri static T InElastic(T t)
{
T t2 = t * t;
return t2 * t2 * std::sin(t * pi * 4.5f);
}
template<typename T>
__ri static T OutElastic(T t)
{
T t2 = (t - 1) * (t - 1);
return 1 - t2 * t2 * std::cos(t * pi * 4.5f);
}
template<typename T>
__ri static T InOutElastic(T t)
{
T t2;
if (t < 0.45f)
{
t2 = t * t;
return 8 * t2 * t2 * std::sin(t * pi * 9);
}
else if (t < 0.55f)
{
return 0.5f + 0.75f * std::sin(t * pi * 4);
}
else
{
t2 = (t - 1) * (t - 1);
return 1 - 8 * t2 * t2 * std::sin(t * pi * 9);
}
}
template<typename T>
__ri static T InBounce(T t)
{
return std::pow(2, 6 * (t - 1)) * std::abs(sin(t * pi * 3.5f));
}
template<typename T>
__ri static T OutBounce(T t)
{
return 1 - std::pow(2, -6 * t) * std::abs(std::cos(t * pi * 3.5f));
}
template<typename T>
__ri static T InOutBounce(T t)
{
if (t < 0.5f)
{
return 8 * std::pow(2, 8 * (t - 1)) * std::abs(std::sin(t * pi * 7));
}
else
{
return 1 - 8 * std::pow(2, -8 * t) * std::abs(std::sin(t * pi * 7));
}
}
} // namespace Easing

91
common/EnumOps.h Normal file
View File

@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <type_traits>
// Template function for casting enumerations to their underlying type
template <typename Enumeration>
typename std::underlying_type<Enumeration>::type enum_cast(Enumeration E)
{
return static_cast<typename std::underlying_type<Enumeration>::type>(E);
}
namespace detail
{
/// Marks an enum as supporting boolean operators
template <typename T>
struct enum_is_flags : public std::false_type {};
/// For return types that should be convertible to bool
template <typename Enum>
struct enum_bool_helper
{
Enum value;
constexpr enum_bool_helper(Enum value): value(value) {}
constexpr operator Enum() const { return value; }
constexpr operator bool() const { return static_cast<bool>(static_cast<typename std::underlying_type<Enum>::type>(value)); }
};
};
#define MARK_ENUM_AS_FLAGS(T) template<> struct detail::enum_is_flags<T> : public std::true_type {}
template <typename Enum>
constexpr typename std::enable_if<detail::enum_is_flags<Enum>::value, Enum>::type
operator|(Enum lhs, Enum rhs) noexcept
{
using underlying = typename std::underlying_type<Enum>::type;
return static_cast<Enum>(static_cast<underlying>(lhs) | static_cast<underlying>(rhs));
}
template <typename Enum>
constexpr typename std::enable_if<detail::enum_is_flags<Enum>::value, detail::enum_bool_helper<Enum>>::type
operator&(Enum lhs, Enum rhs) noexcept
{
using underlying = typename std::underlying_type<Enum>::type;
return static_cast<Enum>(static_cast<underlying>(lhs) & static_cast<underlying>(rhs));
}
template <typename Enum>
constexpr typename std::enable_if<detail::enum_is_flags<Enum>::value, Enum>::type
operator^(Enum lhs, Enum rhs) noexcept
{
using underlying = typename std::underlying_type<Enum>::type;
return static_cast<Enum>(static_cast<underlying>(lhs) ^ static_cast<underlying>(rhs));
}
template <typename Enum>
constexpr typename std::enable_if<detail::enum_is_flags<Enum>::value, Enum&>::type
operator|=(Enum& lhs, Enum rhs) noexcept
{
return lhs = lhs | rhs;
}
template <typename Enum>
constexpr typename std::enable_if<detail::enum_is_flags<Enum>::value, Enum&>::type
operator&=(Enum& lhs, Enum rhs) noexcept
{
return lhs = lhs & rhs;
}
template <typename Enum>
constexpr typename std::enable_if<detail::enum_is_flags<Enum>::value, Enum&>::type
operator^=(Enum& lhs, Enum rhs) noexcept
{
return lhs = lhs ^ rhs;
}
template<typename Enum>
constexpr typename std::enable_if<detail::enum_is_flags<Enum>::value, bool>::type
operator!(Enum e) noexcept
{
return !static_cast<typename std::underlying_type<Enum>::type>(e);
}
template<typename Enum>
constexpr typename std::enable_if<detail::enum_is_flags<Enum>::value, Enum>::type
operator~(Enum e) noexcept
{
return static_cast<Enum>(~static_cast<typename std::underlying_type<Enum>::type>(e));
}

286
common/Error.cpp Normal file
View File

@@ -0,0 +1,286 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Error.h"
#include "StringUtil.h"
#include "fmt/format.h"
#include <cstdlib>
#include <cstring>
#include <cwctype>
#include <type_traits>
// Platform-specific includes
#if defined(_WIN32)
#include "RedtapeWindows.h"
static_assert(std::is_same<DWORD, unsigned long>::value, "DWORD is unsigned long");
static_assert(std::is_same<HRESULT, long>::value, "HRESULT is long");
#endif
Error::Error() = default;
Error::Error(const Error& c) = default;
Error::Error(Error&& e) = default;
Error::~Error() = default;
void Error::Clear()
{
m_description = {};
}
void Error::Clear(Error* errptr)
{
if (errptr)
errptr->Clear();
}
void Error::SetErrno(int err)
{
SetErrno(std::string_view(), err);
}
void Error::SetErrno(std::string_view prefix, int err)
{
m_type = Type::Errno;
#ifdef _MSC_VER
char buf[128];
if (strerror_s(buf, sizeof(buf), err) == 0)
m_description = fmt::format("{}errno {}: {}", prefix, err, buf);
else
m_description = fmt::format("{}errno {}: <Could not get error message>", prefix, err);
#else
const char* buf = std::strerror(err);
if (buf)
m_description = fmt::format("{}errno {}: {}", prefix, err, buf);
else
m_description = fmt::format("{}errno {}: <Could not get error message>", prefix, err);
#endif
}
void Error::SetErrno(Error* errptr, int err)
{
if (errptr)
errptr->SetErrno(err);
}
void Error::SetErrno(Error* errptr, std::string_view prefix, int err)
{
if (errptr)
errptr->SetErrno(prefix, err);
}
void Error::SetString(std::string description)
{
m_type = Type::User;
m_description = std::move(description);
}
void Error::SetStringView(std::string_view description)
{
m_type = Type::User;
m_description = std::string(description);
}
void Error::SetString(Error* errptr, std::string description)
{
if (errptr)
errptr->SetString(std::move(description));
}
void Error::SetStringView(Error* errptr, std::string_view description)
{
if (errptr)
errptr->SetStringView(std::move(description));
}
#ifdef _WIN32
void Error::SetWin32(unsigned long err)
{
SetWin32(std::string_view(), err);
}
void Error::SetWin32(std::string_view prefix, unsigned long err)
{
m_type = Type::Win32;
WCHAR buf[128];
DWORD r = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, err, LANG_USER_DEFAULT, buf,
static_cast<DWORD>(std::size(buf)), nullptr);
while (r > 0 && std::iswspace(buf[r - 1]))
r--;
if (r > 0)
{
m_description =
fmt::format("{}Win32 Error {}: {}", prefix, err, StringUtil::WideStringToUTF8String(std::wstring_view(buf, r)));
}
else
{
m_description = fmt::format("{}Win32 Error {}: <Could not resolve system error ID>", prefix, err);
}
}
void Error::SetWin32(Error* errptr, unsigned long err)
{
if (errptr)
errptr->SetWin32(err);
}
void Error::SetWin32(Error* errptr, std::string_view prefix, unsigned long err)
{
if (errptr)
errptr->SetWin32(prefix, err);
}
void Error::SetHResult(long err)
{
SetHResult(std::string_view(), err);
}
void Error::SetHResult(std::string_view prefix, long err)
{
m_type = Type::HResult;
WCHAR buf[128];
DWORD r = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, err, LANG_USER_DEFAULT, buf,
static_cast<DWORD>(std::size(buf)), nullptr);
while (r > 0 && std::iswspace(buf[r - 1]))
r--;
if (r > 0)
{
m_description = fmt::format("{}HRESULT {:08X}: {}", prefix, static_cast<unsigned>(err),
StringUtil::WideStringToUTF8String(std::wstring_view(buf, r)));
}
else
{
m_description = fmt::format("{}HRESULT {:08X}: <Could not resolve system error ID>", prefix,
static_cast<unsigned>(err));
}
}
void Error::SetHResult(Error* errptr, long err)
{
if (errptr)
errptr->SetHResult(err);
}
void Error::SetHResult(Error* errptr, std::string_view prefix, long err)
{
if (errptr)
errptr->SetHResult(prefix, err);
}
#endif
void Error::SetSocket(int err)
{
SetSocket(std::string_view(), err);
}
void Error::SetSocket(std::string_view prefix, int err)
{
// Socket errors are win32 errors on windows
#ifdef _WIN32
SetWin32(prefix, err);
#else
SetErrno(prefix, err);
#endif
m_type = Type::Socket;
}
void Error::SetSocket(Error* errptr, int err)
{
if (errptr)
errptr->SetSocket(err);
}
void Error::SetSocket(Error* errptr, std::string_view prefix, int err)
{
if (errptr)
errptr->SetSocket(prefix, err);
}
Error Error::CreateNone()
{
return Error();
}
Error Error::CreateErrno(int err)
{
Error ret;
ret.SetErrno(err);
return ret;
}
Error Error::CreateSocket(int err)
{
Error ret;
ret.SetSocket(err);
return ret;
}
Error Error::CreateString(std::string description)
{
Error ret;
ret.SetString(std::move(description));
return ret;
}
#ifdef _WIN32
Error Error::CreateWin32(unsigned long err)
{
Error ret;
ret.SetWin32(err);
return ret;
}
Error Error::CreateHResult(long err)
{
Error ret;
ret.SetHResult(err);
return ret;
}
#endif
void Error::AddPrefix(std::string_view prefix)
{
m_description.insert(0, prefix);
}
void Error::AddSuffix(std::string_view suffix)
{
m_description.append(suffix);
}
void Error::AddPrefix(Error* errptr, std::string_view prefix)
{
if (errptr)
errptr->AddPrefix(prefix);
}
void Error::AddSuffix(Error* errptr, std::string_view prefix)
{
if (errptr)
errptr->AddSuffix(prefix);
}
Error& Error::operator=(const Error& e) = default;
Error& Error::operator=(Error&& e) = default;
bool Error::operator==(const Error& e) const
{
return (m_type == e.m_type && m_description == e.m_description);
}
bool Error::operator!=(const Error& e) const
{
return (m_type != e.m_type || m_description != e.m_description);
}

105
common/Error.h Normal file
View File

@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include "fmt/format.h"
#include <string>
class Error
{
public:
Error();
Error(const Error& e);
Error(Error&& e);
~Error();
enum class Type
{
None = 0,
Errno = 1,
Socket = 2,
User = 3,
Win32 = 4,
HResult = 5,
};
__fi Type GetType() const { return m_type; }
__fi bool IsValid() const { return (m_type != Type::None); }
__fi const std::string& GetDescription() const { return m_description; }
void Clear();
/// Error that is set by system functions, such as open().
void SetErrno(int err);
void SetErrno(std::string_view prefix, int err);
/// Error that is set by socket functions, such as socket(). On Unix this is the same as errno.
void SetSocket(int err);
void SetSocket(std::string_view prefix, int err);
/// Set both description and message.
void SetString(std::string description);
void SetStringView(std::string_view description);
#ifdef _WIN32
/// Error that is returned by some Win32 functions, such as RegOpenKeyEx. Also used by other APIs through
/// GetLastError().
void SetWin32(unsigned long err);
void SetWin32(std::string_view prefix, unsigned long err);
/// Error that is returned by Win32 COM methods, e.g. S_OK.
void SetHResult(long err);
void SetHResult(std::string_view prefix, long err);
#endif
static Error CreateNone();
static Error CreateErrno(int err);
static Error CreateSocket(int err);
static Error CreateString(std::string description);
#ifdef _WIN32
static Error CreateWin32(unsigned long err);
static Error CreateHResult(long err);
#endif
// helpers for setting
static void Clear(Error* errptr);
static void SetErrno(Error* errptr, int err);
static void SetErrno(Error* errptr, std::string_view prefix, int err);
static void SetSocket(Error* errptr, int err);
static void SetSocket(Error* errptr, std::string_view prefix, int err);
static void SetString(Error* errptr, std::string description);
static void SetStringView(Error* errptr, std::string_view description);
#ifdef _WIN32
static void SetWin32(Error* errptr, unsigned long err);
static void SetWin32(Error* errptr, std::string_view prefix, unsigned long err);
static void SetHResult(Error* errptr, long err);
static void SetHResult(Error* errptr, std::string_view prefix, long err);
#endif
/// Sets a formatted message.
template <typename... T>
static void SetStringFmt(Error* errptr, fmt::format_string<T...> fmt, T&&... args)
{
if (errptr)
Error::SetString(errptr, fmt::vformat(fmt, fmt::make_format_args(args...)));
}
void AddPrefix(std::string_view prefix);
void AddSuffix(std::string_view suffix);
static void AddPrefix(Error* errptr, std::string_view prefix);
static void AddSuffix(Error* errptr, std::string_view prefix);
Error& operator=(const Error& e);
Error& operator=(Error&& e);
bool operator==(const Error& e) const;
bool operator!=(const Error& e) const;
private:
Type m_type = Type::None;
std::string m_description;
};

216
common/FPControl.h Normal file
View File

@@ -0,0 +1,216 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// This file abstracts the floating-point control registers, known as MXCSR on x86, and FPCR on AArch64.
#pragma once
#include "common/Pcsx2Defs.h"
#include "common/VectorIntrin.h"
enum class FPRoundMode : u8
{
Nearest,
NegativeInfinity,
PositiveInfinity,
ChopZero,
MaxCount
};
struct FPControlRegister
{
#ifdef _M_X86
u32 bitmask;
static constexpr u32 EXCEPTION_MASK = (0x3Fu << 7);
static constexpr u32 ROUNDING_CONTROL_SHIFT = 13;
static constexpr u32 ROUNDING_CONTROL_MASK = 3u;
static constexpr u32 ROUNDING_CONTROL_BITS = (ROUNDING_CONTROL_MASK << ROUNDING_CONTROL_SHIFT);
static constexpr u32 DENORMALS_ARE_ZERO_BIT = (1u << 6);
static constexpr u32 FLUSH_TO_ZERO_BIT = (1u << 15);
__fi static FPControlRegister GetCurrent()
{
return FPControlRegister{_mm_getcsr()};
}
__fi static void SetCurrent(FPControlRegister value)
{
_mm_setcsr(value.bitmask);
}
__fi static constexpr FPControlRegister GetDefault()
{
// 0x1f80 - all exceptions masked, nearest rounding
return FPControlRegister{0x1f80};
}
__fi constexpr FPControlRegister& EnableExceptions()
{
bitmask &= ~EXCEPTION_MASK;
return *this;
}
__fi constexpr FPControlRegister DisableExceptions()
{
bitmask |= EXCEPTION_MASK;
return *this;
}
__fi constexpr FPRoundMode GetRoundMode() const
{
return static_cast<FPRoundMode>((bitmask >> ROUNDING_CONTROL_SHIFT) & ROUNDING_CONTROL_MASK);
}
__fi constexpr FPControlRegister& SetRoundMode(FPRoundMode mode)
{
// These bits match on x86.
bitmask = (bitmask & ~ROUNDING_CONTROL_BITS) | ((static_cast<u32>(mode) & ROUNDING_CONTROL_MASK) << ROUNDING_CONTROL_SHIFT);
return *this;
}
__fi constexpr bool GetDenormalsAreZero() const
{
return ((bitmask & DENORMALS_ARE_ZERO_BIT) != 0);
}
__fi constexpr FPControlRegister SetDenormalsAreZero(bool daz)
{
if (daz)
bitmask |= DENORMALS_ARE_ZERO_BIT;
else
bitmask &= ~DENORMALS_ARE_ZERO_BIT;
return *this;
}
__fi constexpr bool GetFlushToZero() const
{
return ((bitmask & FLUSH_TO_ZERO_BIT) != 0);
}
__fi constexpr FPControlRegister SetFlushToZero(bool ftz)
{
if (ftz)
bitmask |= FLUSH_TO_ZERO_BIT;
else
bitmask &= ~FLUSH_TO_ZERO_BIT;
return *this;
}
__fi constexpr bool operator==(const FPControlRegister& rhs) const { return bitmask == rhs.bitmask; }
__fi constexpr bool operator!=(const FPControlRegister& rhs) const { return bitmask != rhs.bitmask; }
#elif defined(_M_ARM64)
u64 bitmask;
static constexpr u64 FZ_BIT = (0x1ULL << 24);
static constexpr u32 RMODE_SHIFT = 22;
static constexpr u64 RMODE_MASK = 0x3ULL;
static constexpr u64 RMODE_BITS = (RMODE_MASK << RMODE_SHIFT);
static constexpr u32 EXCEPTION_MASK = (0x3Fu << 5);
__fi static FPControlRegister GetCurrent()
{
u64 value;
asm volatile("\tmrs %0, FPCR\n"
: "=r"(value));
return FPControlRegister{value};
}
__fi static void SetCurrent(FPControlRegister value)
{
asm volatile("\tmsr FPCR, %0\n" ::"r"(value.bitmask));
}
__fi static constexpr FPControlRegister GetDefault()
{
// 0x0 - all exceptions masked, nearest rounding
return FPControlRegister{0x0};
}
__fi constexpr FPControlRegister& EnableExceptions()
{
bitmask |= EXCEPTION_MASK;
return *this;
}
__fi constexpr FPControlRegister& DisableExceptions()
{
bitmask &= ~EXCEPTION_MASK;
return *this;
}
__fi constexpr FPRoundMode GetRoundMode() const
{
// Negative/Positive infinity rounding is flipped on A64.
const u64 RMode = (bitmask >> RMODE_SHIFT) & RMODE_MASK;
return static_cast<FPRoundMode>((RMode == 0b00 || RMode == 0b11) ? RMode : (RMode ^ 0b11));
}
__fi constexpr FPControlRegister& SetRoundMode(FPRoundMode mode)
{
const u64 RMode = ((mode == FPRoundMode::Nearest || mode == FPRoundMode::ChopZero) ? static_cast<u64>(mode) : (static_cast<u64>(mode) ^ 0b11));
bitmask = (bitmask & ~RMODE_BITS) | ((RMode & RMODE_MASK) << RMODE_SHIFT);
return *this;
}
__fi constexpr bool GetDenormalsAreZero() const
{
// Without FEAT_AFP, most ARM chips don't have separate DaZ/FtZ. This includes Apple Silicon, which
// implements x86-like behavior with a vendor-specific extension that we cannot access from usermode.
// The FZ bit causes both inputs and outputs to be flushed to zero.
return ((bitmask & FZ_BIT) != 0);
}
__fi constexpr FPControlRegister SetDenormalsAreZero(bool daz)
{
if (daz)
bitmask |= FZ_BIT;
else
bitmask &= ~FZ_BIT;
return *this;
}
__fi constexpr bool GetFlushToZero() const
{
// See note in GetDenormalsAreZero().
return ((bitmask & FZ_BIT) != 0);
}
__fi constexpr FPControlRegister SetFlushToZero(bool ftz)
{
if (ftz)
bitmask |= FZ_BIT;
else
bitmask &= ~FZ_BIT;
return *this;
}
__fi constexpr bool operator==(const FPControlRegister& rhs) const { return bitmask == rhs.bitmask; }
__fi constexpr bool operator!=(const FPControlRegister& rhs) const { return bitmask != rhs.bitmask; }
#else
#error Unknown architecture.
#endif
};
/// Helper to back up/restore FPCR.
class FPControlRegisterBackup
{
public:
__fi FPControlRegisterBackup(FPControlRegister new_value)
: m_prev_val(FPControlRegister::GetCurrent())
{
FPControlRegister::SetCurrent(new_value);
}
__fi ~FPControlRegisterBackup()
{
FPControlRegister::SetCurrent(m_prev_val);
}
FPControlRegisterBackup(const FPControlRegisterBackup&) = delete;
FPControlRegisterBackup& operator=(const FPControlRegisterBackup&) = delete;
private:
FPControlRegister m_prev_val;
};

92
common/FastJmp.cpp Normal file
View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "FastJmp.h"
// Win32 uses Fastjmp.asm, because MSVC doesn't support inline asm.
#if !defined(_WIN32) || defined(_M_ARM64)
#if defined(__APPLE__)
#define PREFIX "_"
#else
#define PREFIX ""
#endif
#if defined(_M_X86)
asm(
"\t.global " PREFIX "fastjmp_set\n"
"\t.global " PREFIX "fastjmp_jmp\n"
"\t.text\n"
"\t" PREFIX "fastjmp_set:" R"(
movq 0(%rsp), %rax
movq %rsp, %rdx # fixup stack pointer, so it doesn't include the call to fastjmp_set
addq $8, %rdx
movq %rax, 0(%rdi) # actually rip
movq %rbx, 8(%rdi)
movq %rdx, 16(%rdi) # actually rsp
movq %rbp, 24(%rdi)
movq %r12, 32(%rdi)
movq %r13, 40(%rdi)
movq %r14, 48(%rdi)
movq %r15, 56(%rdi)
xorl %eax, %eax
ret
)"
"\t" PREFIX "fastjmp_jmp:" R"(
movl %esi, %eax
movq 0(%rdi), %rdx # actually rip
movq 8(%rdi), %rbx
movq 16(%rdi), %rsp # actually rsp
movq 24(%rdi), %rbp
movq 32(%rdi), %r12
movq 40(%rdi), %r13
movq 48(%rdi), %r14
movq 56(%rdi), %r15
jmp *%rdx
)");
#elif defined(_M_ARM64)
asm(
"\t.global " PREFIX "fastjmp_set\n"
"\t.global " PREFIX "fastjmp_jmp\n"
"\t.text\n"
"\t.align 16\n"
"\t" PREFIX "fastjmp_set:" R"(
mov x16, sp
stp x16, x30, [x0]
stp x19, x20, [x0, #16]
stp x21, x22, [x0, #32]
stp x23, x24, [x0, #48]
stp x25, x26, [x0, #64]
stp x27, x28, [x0, #80]
str x29, [x0, #96]
stp d8, d9, [x0, #112]
stp d10, d11, [x0, #128]
stp d12, d13, [x0, #144]
stp d14, d15, [x0, #160]
mov w0, wzr
br x30
)"
".align 16\n"
"\t" PREFIX "fastjmp_jmp:" R"(
ldp x16, x30, [x0]
mov sp, x16
ldp x19, x20, [x0, #16]
ldp x21, x22, [x0, #32]
ldp x23, x24, [x0, #48]
ldp x25, x26, [x0, #64]
ldp x27, x28, [x0, #80]
ldr x29, [x0, #96]
ldp d8, d9, [x0, #112]
ldp d10, d11, [x0, #128]
ldp d12, d13, [x0, #144]
ldp d14, d15, [x0, #160]
mov w0, w1
br x30
)");
#endif
#endif // __WIN32

25
common/FastJmp.h Normal file
View File

@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <cstdint>
#include <cstddef>
struct fastjmp_buf
{
#if defined(_WIN32)
static constexpr std::size_t BUF_SIZE = 240;
#elif defined(_M_ARM64)
static constexpr std::size_t BUF_SIZE = 168;
#else
static constexpr std::size_t BUF_SIZE = 64;
#endif
alignas(16) std::uint8_t buf[BUF_SIZE];
};
extern "C" {
int fastjmp_set(fastjmp_buf* buf);
__noreturn void fastjmp_jmp(const fastjmp_buf* buf, int ret);
}

2646
common/FileSystem.cpp Normal file

File diff suppressed because it is too large Load Diff

212
common/FileSystem.h Normal file
View File

@@ -0,0 +1,212 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <cstdio>
#include <ctime>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <sys/stat.h>
class Error;
class ProgressCallback;
#ifdef _WIN32
#define FS_OSPATH_SEPARATOR_CHARACTER '\\'
#define FS_OSPATH_SEPARATOR_STR "\\"
#else
#define FS_OSPATH_SEPARATOR_CHARACTER '/'
#define FS_OSPATH_SEPARATOR_STR "/"
#endif
enum FILESYSTEM_FILE_ATTRIBUTES
{
FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY = 1,
FILESYSTEM_FILE_ATTRIBUTE_READ_ONLY = 2,
FILESYSTEM_FILE_ATTRIBUTE_COMPRESSED = 4,
};
enum FILESYSTEM_FIND_FLAGS
{
FILESYSTEM_FIND_RECURSIVE = (1 << 0),
FILESYSTEM_FIND_RELATIVE_PATHS = (1 << 1),
FILESYSTEM_FIND_HIDDEN_FILES = (1 << 2),
FILESYSTEM_FIND_FOLDERS = (1 << 3),
FILESYSTEM_FIND_FILES = (1 << 4),
FILESYSTEM_FIND_KEEP_ARRAY = (1 << 5),
FILESYSTEM_FIND_SORT_BY_NAME = (1 << 6),
};
struct FILESYSTEM_STAT_DATA
{
std::time_t CreationTime; // actually inode change time on linux
std::time_t ModificationTime;
s64 Size;
u32 Attributes;
};
struct FILESYSTEM_FIND_DATA
{
std::time_t CreationTime; // actually inode change time on linux
std::time_t ModificationTime;
std::string FileName;
s64 Size;
u32 Attributes;
};
namespace FileSystem
{
using FindResultsArray = std::vector<FILESYSTEM_FIND_DATA>;
/// Returns a list of "root directories" (i.e. root/home directories on Linux, drive letters on Windows).
std::vector<std::string> GetRootDirectoryList();
/// Search for files
bool FindFiles(const char* path, const char* pattern, u32 flags, FindResultsArray* results, ProgressCallback* cancel = nullptr);
/// Stat file
bool StatFile(const char* path, struct stat* st);
bool StatFile(std::FILE* fp, struct stat* st);
bool StatFile(const char* path, FILESYSTEM_STAT_DATA* pStatData);
bool StatFile(std::FILE* fp, FILESYSTEM_STAT_DATA* pStatData);
s64 GetPathFileSize(const char* path);
/// Returns the last modified timestamp for a file.
std::optional<std::time_t> GetFileTimestamp(const char* path);
/// File exists?
bool FileExists(const char* path);
/// Directory exists?
bool DirectoryExists(const char* path);
/// Directory does not contain any files?
bool DirectoryIsEmpty(const char* path);
/// Delete file
bool DeleteFilePath(const char* path, Error* error = nullptr);
/// Rename file
bool RenamePath(const char* OldPath, const char* NewPath, Error* error = nullptr);
/// Deleter functor for managed file pointers
struct FileDeleter
{
void operator()(std::FILE* fp)
{
std::fclose(fp);
}
};
/// open files
using ManagedCFilePtr = std::unique_ptr<std::FILE, FileDeleter>;
ManagedCFilePtr OpenManagedCFile(const char* filename, const char* mode, Error* error = nullptr);
// Tries to open a file using the given filename, but if that fails searches
// the directory for a file with a case-insensitive match.
// This is the same as OpenManagedCFile on Windows and MacOS
ManagedCFilePtr OpenManagedCFileTryIgnoreCase(const char* filename, const char* mode, Error* error = nullptr);
std::FILE* OpenCFile(const char* filename, const char* mode, Error* error = nullptr);
// Tries to open a file using the given filename, but if that fails searches
// the directory for a file with a case-insensitive match.
// This is the same as OpenCFile on Windows and MacOS
std::FILE* OpenCFileTryIgnoreCase(const char* filename, const char* mode, Error* error = nullptr);
int FSeek64(std::FILE* fp, s64 offset, int whence);
s64 FTell64(std::FILE* fp);
s64 FSize64(std::FILE* fp);
int OpenFDFile(const char* filename, int flags, int mode, Error* error = nullptr);
/// Sharing modes for OpenSharedCFile().
enum class FileShareMode
{
DenyReadWrite, /// Exclusive access.
DenyWrite, /// Other processes can read from this file.
DenyRead, /// Other processes can write to this file.
DenyNone, /// Other processes can read and write to this file.
};
/// Opens a file in shareable mode (where other processes can access it concurrently).
/// Only has an effect on Windows systems.
ManagedCFilePtr OpenManagedSharedCFile(const char* filename, const char* mode, FileShareMode share_mode, Error* error = nullptr);
std::FILE* OpenSharedCFile(const char* filename, const char* mode, FileShareMode share_mode, Error* error = nullptr);
std::optional<std::vector<u8>> ReadBinaryFile(const char* filename);
std::optional<std::vector<u8>> ReadBinaryFile(std::FILE* fp);
std::optional<std::string> ReadFileToString(const char* filename);
std::optional<std::string> ReadFileToString(std::FILE* fp);
bool WriteBinaryFile(const char* filename, const void* data, size_t data_length);
bool WriteStringToFile(const char* filename, const std::string_view sv);
size_t ReadFileWithProgress(std::FILE* fp, void* dst, size_t length, ProgressCallback* progress,
Error* error = nullptr, size_t chunk_size = 16 * 1024 * 1024);
size_t ReadFileWithPartialProgress(std::FILE* fp, void* dst, size_t length, ProgressCallback* progress,
int startPercent, int endPercent, Error* error = nullptr, size_t chunk_size = 16 * 1024 * 1024);
/// creates a directory in the local filesystem
/// if the directory already exists, the return value will be true.
/// if Recursive is specified, all parent directories will be created
/// if they do not exist.
bool CreateDirectoryPath(const char* path, bool recursive, Error* error = nullptr);
/// Creates a directory if it doesn't already exist.
/// Returns false if it does not exist and creation failed.
bool EnsureDirectoryExists(const char* path, bool recursive, Error* error = nullptr);
/// Removes a directory.
bool DeleteDirectory(const char* path);
/// Recursively removes a directory and all subdirectories/files.
bool RecursiveDeleteDirectory(const char* path);
/// Copies one file to another, optionally replacing it if it already exists.
bool CopyFilePath(const char* source, const char* destination, bool replace);
/// Returns the path to the current executable.
std::string GetProgramPath();
/// Retrieves the current working directory.
std::string GetWorkingDirectory();
/// Sets the current working directory. Returns true if successful.
bool SetWorkingDirectory(const char* path);
/// Enables/disables NTFS compression on a file or directory.
/// Does not apply the compression flag recursively if called for a directory.
/// Does nothing and returns false on non-Windows platforms.
bool SetPathCompression(const char* path, bool enable);
// Creates a symbolic link. Note that on Windows this requires elevated
// privileges so this is mostly useful for testing purposes.
bool CreateSymLink(const char* link, const char* target);
/// Checks if a file or directory is a symbolic link.
bool IsSymbolicLink(const char* path);
/// Deletes a symbolic link (either a file or directory).
bool DeleteSymbolicLink(const char* path, Error* error = nullptr);
#ifdef _WIN32
// Path limit remover, but also converts to a wide string at the same time.
bool GetWin32Path(std::wstring* dest, std::string_view str);
std::wstring GetWin32Path(std::string_view str);
#endif
/// Abstracts a POSIX file lock.
#ifndef _WIN32
class POSIXLock
{
public:
POSIXLock(int fd);
POSIXLock(std::FILE* fp);
~POSIXLock();
private:
int m_fd;
};
#endif
}; // namespace FileSystem

321
common/HTTPDownloader.cpp Normal file
View File

@@ -0,0 +1,321 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/HTTPDownloader.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/ProgressCallback.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include "common/Threading.h"
static constexpr float DEFAULT_TIMEOUT_IN_SECONDS = 30;
static constexpr u32 DEFAULT_MAX_ACTIVE_REQUESTS = 4;
const char HTTPDownloader::DEFAULT_USER_AGENT[] =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0";
HTTPDownloader::HTTPDownloader()
: m_timeout(DEFAULT_TIMEOUT_IN_SECONDS)
, m_max_active_requests(DEFAULT_MAX_ACTIVE_REQUESTS)
{
}
HTTPDownloader::~HTTPDownloader() = default;
void HTTPDownloader::SetTimeout(float timeout)
{
m_timeout = timeout;
}
void HTTPDownloader::SetMaxActiveRequests(u32 max_active_requests)
{
pxAssert(max_active_requests > 0);
m_max_active_requests = max_active_requests;
}
void HTTPDownloader::CreateRequest(std::string url, Request::Callback callback, ProgressCallback* progress)
{
Request* req = InternalCreateRequest();
req->parent = this;
req->type = Request::Type::Get;
req->url = std::move(url);
req->callback = std::move(callback);
req->progress = progress;
req->start_time = Common::Timer::GetCurrentValue();
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
if (LockedGetActiveRequestCount() < m_max_active_requests)
{
if (!StartRequest(req))
return;
}
LockedAddRequest(req);
}
void HTTPDownloader::CreatePostRequest(std::string url, std::string post_data, Request::Callback callback, ProgressCallback* progress)
{
Request* req = InternalCreateRequest();
req->parent = this;
req->type = Request::Type::Post;
req->url = std::move(url);
req->post_data = std::move(post_data);
req->callback = std::move(callback);
req->progress = progress;
req->start_time = Common::Timer::GetCurrentValue();
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
if (LockedGetActiveRequestCount() < m_max_active_requests)
{
if (!StartRequest(req))
return;
}
LockedAddRequest(req);
}
void HTTPDownloader::LockedPollRequests(std::unique_lock<std::mutex>& lock)
{
if (m_pending_http_requests.empty())
return;
InternalPollRequests();
const Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
u32 active_requests = 0;
u32 unstarted_requests = 0;
for (size_t index = 0; index < m_pending_http_requests.size();)
{
Request* req = m_pending_http_requests[index];
if (req->state == Request::State::Pending)
{
unstarted_requests++;
index++;
continue;
}
if ((req->state == Request::State::Started || req->state == Request::State::Receiving) &&
current_time >= req->start_time && Common::Timer::ConvertValueToSeconds(current_time - req->start_time) >= m_timeout)
{
// request timed out
Console.Error("Request for '%s' timed out", req->url.c_str());
req->state.store(Request::State::Cancelled);
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
lock.unlock();
req->callback(HTTP_STATUS_TIMEOUT, std::string(), Request::Data());
CloseRequest(req);
lock.lock();
continue;
}
else if ((req->state == Request::State::Started || req->state == Request::State::Receiving) && req->progress &&
req->progress->IsCancelled())
{
// request timed out
Console.Error("Request for '%s' cancelled", req->url.c_str());
req->state.store(Request::State::Cancelled);
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
lock.unlock();
req->callback(HTTP_STATUS_CANCELLED, std::string(), Request::Data());
CloseRequest(req);
lock.lock();
continue;
}
if (req->state != Request::State::Complete)
{
if (req->progress)
{
const u32 size = static_cast<u32>(req->data.size());
if (size != req->last_progress_update)
{
req->last_progress_update = size;
req->progress->SetProgressRange(req->content_length);
req->progress->SetProgressValue(req->last_progress_update);
}
}
active_requests++;
index++;
continue;
}
// request complete
DevCon.WriteLn("Request for '%s' complete, returned status code %u and %zu bytes", req->url.c_str(),
req->status_code, req->data.size());
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
// run callback with lock unheld
lock.unlock();
req->callback(req->status_code, req->content_type, std::move(req->data));
CloseRequest(req);
lock.lock();
}
// start new requests when we finished some
if (unstarted_requests > 0 && active_requests < m_max_active_requests)
{
for (size_t index = 0; index < m_pending_http_requests.size();)
{
Request* req = m_pending_http_requests[index];
if (req->state != Request::State::Pending)
{
index++;
continue;
}
if (!StartRequest(req))
{
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
continue;
}
active_requests++;
index++;
if (active_requests >= m_max_active_requests)
break;
}
}
}
void HTTPDownloader::PollRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
LockedPollRequests(lock);
}
void HTTPDownloader::WaitForAllRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
while (!m_pending_http_requests.empty())
{
// Don't burn too much CPU.
Threading::Sleep(1);
LockedPollRequests(lock);
}
}
void HTTPDownloader::LockedAddRequest(Request* request)
{
m_pending_http_requests.push_back(request);
}
u32 HTTPDownloader::LockedGetActiveRequestCount()
{
u32 count = 0;
for (Request* req : m_pending_http_requests)
{
if (req->state == Request::State::Started || req->state == Request::State::Receiving)
count++;
}
return count;
}
bool HTTPDownloader::HasAnyRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
return !m_pending_http_requests.empty();
}
std::string HTTPDownloader::GetExtensionForContentType(const std::string& content_type)
{
// Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
static constexpr const char* table[][2] = {
{"audio/aac", "aac"},
{"application/x-abiword", "abw"},
{"application/x-freearc", "arc"},
{"image/avif", "avif"},
{"video/x-msvideo", "avi"},
{"application/vnd.amazon.ebook", "azw"},
{"application/octet-stream", "bin"},
{"image/bmp", "bmp"},
{"application/x-bzip", "bz"},
{"application/x-bzip2", "bz2"},
{"application/x-cdf", "cda"},
{"application/x-csh", "csh"},
{"text/css", "css"},
{"text/csv", "csv"},
{"application/msword", "doc"},
{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"},
{"application/vnd.ms-fontobject", "eot"},
{"application/epub+zip", "epub"},
{"application/gzip", "gz"},
{"image/gif", "gif"},
{"text/html", "htm"},
{"image/vnd.microsoft.icon", "ico"},
{"text/calendar", "ics"},
{"application/java-archive", "jar"},
{"image/jpeg", "jpg"},
{"text/javascript", "js"},
{"application/json", "json"},
{"application/ld+json", "jsonld"},
{"audio/midi audio/x-midi", "mid"},
{"text/javascript", "mjs"},
{"audio/mpeg", "mp3"},
{"video/mp4", "mp4"},
{"video/mpeg", "mpeg"},
{"application/vnd.apple.installer+xml", "mpkg"},
{"application/vnd.oasis.opendocument.presentation", "odp"},
{"application/vnd.oasis.opendocument.spreadsheet", "ods"},
{"application/vnd.oasis.opendocument.text", "odt"},
{"audio/ogg", "oga"},
{"video/ogg", "ogv"},
{"application/ogg", "ogx"},
{"audio/opus", "opus"},
{"font/otf", "otf"},
{"image/png", "png"},
{"application/pdf", "pdf"},
{"application/x-httpd-php", "php"},
{"application/vnd.ms-powerpoint", "ppt"},
{"application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"},
{"application/vnd.rar", "rar"},
{"application/rtf", "rtf"},
{"application/x-sh", "sh"},
{"image/svg+xml", "svg"},
{"application/x-tar", "tar"},
{"image/tiff", "tif"},
{"video/mp2t", "ts"},
{"font/ttf", "ttf"},
{"text/plain", "txt"},
{"application/vnd.visio", "vsd"},
{"audio/wav", "wav"},
{"audio/webm", "weba"},
{"video/webm", "webm"},
{"image/webp", "webp"},
{"font/woff", "woff"},
{"font/woff2", "woff2"},
{"application/xhtml+xml", "xhtml"},
{"application/vnd.ms-excel", "xls"},
{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"},
{"application/xml", "xml"},
{"text/xml", "xml"},
{"application/vnd.mozilla.xul+xml", "xul"},
{"application/zip", "zip"},
{"video/3gpp", "3gp"},
{"audio/3gpp", "3gp"},
{"video/3gpp2", "3g2"},
{"audio/3gpp2", "3g2"},
{"application/x-7z-compressed", "7z"},
};
std::string ret;
for (size_t i = 0; i < std::size(table); i++)
{
if (StringUtil::compareNoCase(table[i][0], content_type))
{
ret = table[i][1];
break;
}
}
return ret;
}

98
common/HTTPDownloader.h Normal file
View File

@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Defs.h"
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
class ProgressCallback;
class HTTPDownloader
{
public:
enum : s32
{
HTTP_STATUS_CANCELLED = -3,
HTTP_STATUS_TIMEOUT = -2,
HTTP_STATUS_ERROR = -1,
HTTP_STATUS_OK = 200
};
struct Request
{
using Data = std::vector<u8>;
using Callback = std::function<void(s32 status_code, const std::string& content_type, Data data)>;
enum class Type
{
Get,
Post,
};
enum class State
{
Pending,
Cancelled,
Started,
Receiving,
Complete,
};
HTTPDownloader* parent;
Callback callback;
ProgressCallback* progress;
std::string url;
std::string post_data;
std::string content_type;
Data data;
u64 start_time;
s32 status_code = 0;
u32 content_length = 0;
u32 last_progress_update = 0;
Type type = Type::Get;
std::atomic<State> state{State::Pending};
};
HTTPDownloader();
virtual ~HTTPDownloader();
static std::unique_ptr<HTTPDownloader> Create(std::string user_agent = DEFAULT_USER_AGENT);
static std::string GetExtensionForContentType(const std::string& content_type);
void SetTimeout(float timeout);
void SetMaxActiveRequests(u32 max_active_requests);
void CreateRequest(std::string url, Request::Callback callback, ProgressCallback* progress = nullptr);
void CreatePostRequest(std::string url, std::string post_data, Request::Callback callback, ProgressCallback* progress = nullptr);
void PollRequests();
void WaitForAllRequests();
bool HasAnyRequests();
static const char DEFAULT_USER_AGENT[];
protected:
virtual Request* InternalCreateRequest() = 0;
virtual void InternalPollRequests() = 0;
virtual bool StartRequest(Request* request) = 0;
virtual void CloseRequest(Request* request) = 0;
void LockedAddRequest(Request* request);
u32 LockedGetActiveRequestCount();
void LockedPollRequests(std::unique_lock<std::mutex>& lock);
float m_timeout;
u32 m_max_active_requests;
std::mutex m_pending_http_request_lock;
std::vector<Request*> m_pending_http_requests;
};

View File

@@ -0,0 +1,206 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/HTTPDownloaderCurl.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include "fmt/format.h"
#include <algorithm>
#include <functional>
#include <pthread.h>
#include <signal.h>
HTTPDownloaderCurl::HTTPDownloaderCurl()
: HTTPDownloader()
{
}
HTTPDownloaderCurl::~HTTPDownloaderCurl()
{
if (m_multi_handle)
curl_multi_cleanup(m_multi_handle);
}
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(std::string user_agent)
{
std::unique_ptr<HTTPDownloaderCurl> instance(std::make_unique<HTTPDownloaderCurl>());
if (!instance->Initialize(std::move(user_agent)))
return {};
return instance;
}
static bool s_curl_initialized = false;
static std::once_flag s_curl_initialized_once_flag;
bool HTTPDownloaderCurl::Initialize(std::string user_agent)
{
if (!s_curl_initialized)
{
std::call_once(s_curl_initialized_once_flag, []() {
s_curl_initialized = curl_global_init(CURL_GLOBAL_ALL) == CURLE_OK;
if (s_curl_initialized)
{
std::atexit([]() {
curl_global_cleanup();
s_curl_initialized = false;
});
}
});
if (!s_curl_initialized)
{
Console.Error("curl_global_init() failed");
return false;
}
}
m_multi_handle = curl_multi_init();
if (!m_multi_handle)
{
Console.Error("curl_multi_init() failed");
return false;
}
m_user_agent = std::move(user_agent);
return true;
}
size_t HTTPDownloaderCurl::WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
{
Request* req = static_cast<Request*>(userdata);
const size_t current_size = req->data.size();
const size_t transfer_size = size * nmemb;
const size_t new_size = current_size + transfer_size;
req->data.resize(new_size);
req->start_time = Common::Timer::GetCurrentValue();
std::memcpy(&req->data[current_size], ptr, transfer_size);
if (req->content_length == 0)
{
curl_off_t length;
if (curl_easy_getinfo(req->handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &length) == CURLE_OK)
req->content_length = static_cast<u32>(length);
}
return nmemb;
}
HTTPDownloader::Request* HTTPDownloaderCurl::InternalCreateRequest()
{
Request* req = new Request();
req->handle = curl_easy_init();
if (!req->handle)
{
delete req;
return nullptr;
}
return req;
}
void HTTPDownloaderCurl::InternalPollRequests()
{
// Apparently OpenSSL can fire SIGPIPE...
sigset_t old_block_mask = {};
sigset_t new_block_mask = {};
sigemptyset(&old_block_mask);
sigemptyset(&new_block_mask);
sigaddset(&new_block_mask, SIGPIPE);
if (pthread_sigmask(SIG_BLOCK, &new_block_mask, &old_block_mask) != 0)
Console.Warning("Failed to block SIGPIPE");
int running_handles;
const CURLMcode err = curl_multi_perform(m_multi_handle, &running_handles);
if (err != CURLM_OK)
Console.Error(fmt::format("curl_multi_perform() returned {}", static_cast<int>(err)));
for (;;)
{
int msgq;
struct CURLMsg* msg = curl_multi_info_read(m_multi_handle, &msgq);
if (!msg)
break;
if (msg->msg != CURLMSG_DONE)
{
Console.Warning(fmt::format("Unexpected multi message {}", static_cast<int>(msg->msg)));
continue;
}
Request* req;
if (curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &req) != CURLE_OK)
{
Console.Error("curl_easy_getinfo() failed");
continue;
}
if (msg->data.result == CURLE_OK)
{
long response_code = 0;
curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response_code);
req->status_code = static_cast<s32>(response_code);
char* content_type = nullptr;
if (curl_easy_getinfo(req->handle, CURLINFO_CONTENT_TYPE, &content_type) == CURLE_OK && content_type)
req->content_type = content_type;
DevCon.WriteLn(fmt::format("Request for '{}' returned status code {} and {} bytes", req->url, req->status_code, req->data.size()));
}
else
{
Console.Error(fmt::format("Request for '{}' returned error {}", req->url, static_cast<int>(msg->data.result)));
}
req->state.store(Request::State::Complete, std::memory_order_release);
}
if (pthread_sigmask(SIG_UNBLOCK, &new_block_mask, &old_block_mask) != 0)
Console.Warning("Failed to unblock SIGPIPE");
}
bool HTTPDownloaderCurl::StartRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
curl_easy_setopt(req->handle, CURLOPT_URL, request->url.c_str());
curl_easy_setopt(req->handle, CURLOPT_USERAGENT, m_user_agent.c_str());
curl_easy_setopt(req->handle, CURLOPT_WRITEFUNCTION, &HTTPDownloaderCurl::WriteCallback);
curl_easy_setopt(req->handle, CURLOPT_WRITEDATA, req);
curl_easy_setopt(req->handle, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(req->handle, CURLOPT_PRIVATE, req);
curl_easy_setopt(req->handle, CURLOPT_FOLLOWLOCATION, 1L);
if (request->type == Request::Type::Post)
{
curl_easy_setopt(req->handle, CURLOPT_POST, 1L);
curl_easy_setopt(req->handle, CURLOPT_POSTFIELDS, request->post_data.c_str());
}
DevCon.WriteLn(fmt::format("Started HTTP request for '{}'", req->url));
req->state.store(Request::State::Started, std::memory_order_release);
req->start_time = Common::Timer::GetCurrentValue();
const CURLMcode err = curl_multi_add_handle(m_multi_handle, req->handle);
if (err != CURLM_OK)
{
Console.Error(fmt::format("curl_multi_add_handle() returned {}", static_cast<int>(err)));
req->callback(HTTP_STATUS_ERROR, std::string(), req->data);
curl_easy_cleanup(req->handle);
delete req;
return false;
}
return true;
}
void HTTPDownloaderCurl::CloseRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
pxAssert(req->handle);
curl_multi_remove_handle(m_multi_handle, req->handle);
curl_easy_cleanup(req->handle);
delete req;
}

View File

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/HTTPDownloader.h"
#include <atomic>
#include <memory>
#include <mutex>
#include <curl/curl.h>
class HTTPDownloaderCurl final : public HTTPDownloader
{
public:
HTTPDownloaderCurl();
~HTTPDownloaderCurl() override;
bool Initialize(std::string user_agent);
protected:
Request* InternalCreateRequest() override;
void InternalPollRequests() override;
bool StartRequest(HTTPDownloader::Request* request) override;
void CloseRequest(HTTPDownloader::Request* request) override;
private:
struct Request : HTTPDownloader::Request
{
CURL* handle = nullptr;
};
static size_t WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata);
CURLM* m_multi_handle = nullptr;
std::string m_user_agent;
};

View File

@@ -0,0 +1,316 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/HTTPDownloaderWinHTTP.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include <VersionHelpers.h>
#include <algorithm>
#pragma comment(lib, "winhttp.lib")
HTTPDownloaderWinHttp::HTTPDownloaderWinHttp()
: HTTPDownloader()
{
}
HTTPDownloaderWinHttp::~HTTPDownloaderWinHttp()
{
if (m_hSession)
{
WinHttpSetStatusCallback(m_hSession, nullptr, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, NULL);
WinHttpCloseHandle(m_hSession);
}
}
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(std::string user_agent)
{
std::unique_ptr<HTTPDownloaderWinHttp> instance(std::make_unique<HTTPDownloaderWinHttp>());
if (!instance->Initialize(std::move(user_agent)))
return {};
return instance;
}
bool HTTPDownloaderWinHttp::Initialize(std::string user_agent)
{
const DWORD dwAccessType = WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY;
m_hSession = WinHttpOpen(StringUtil::UTF8StringToWideString(user_agent).c_str(), dwAccessType, nullptr, nullptr,
WINHTTP_FLAG_ASYNC);
if (m_hSession == NULL)
{
Console.Error("WinHttpOpen() failed: %u", GetLastError());
return false;
}
const DWORD notification_flags = WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | WINHTTP_CALLBACK_FLAG_REQUEST_ERROR |
WINHTTP_CALLBACK_FLAG_HANDLES | WINHTTP_CALLBACK_FLAG_SECURE_FAILURE;
if (WinHttpSetStatusCallback(m_hSession, HTTPStatusCallback, notification_flags, NULL) ==
WINHTTP_INVALID_STATUS_CALLBACK)
{
Console.Error("WinHttpSetStatusCallback() failed: %u", GetLastError());
return false;
}
return true;
}
void CALLBACK HTTPDownloaderWinHttp::HTTPStatusCallback(HINTERNET hRequest, DWORD_PTR dwContext, DWORD dwInternetStatus,
LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
Request* req = reinterpret_cast<Request*>(dwContext);
switch (dwInternetStatus)
{
case WINHTTP_CALLBACK_STATUS_HANDLE_CREATED:
return;
case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING:
{
if (!req)
return;
pxAssert(hRequest == req->hRequest);
HTTPDownloaderWinHttp* parent = static_cast<HTTPDownloaderWinHttp*>(req->parent);
std::unique_lock<std::mutex> lock(parent->m_pending_http_request_lock);
pxAssertRel(std::none_of(parent->m_pending_http_requests.begin(), parent->m_pending_http_requests.end(),
[req](HTTPDownloader::Request* it) { return it == req; }),
"Request is not pending at close time");
// we can clean up the connection as well
pxAssert(req->hConnection != NULL);
WinHttpCloseHandle(req->hConnection);
delete req;
return;
}
case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
{
const WINHTTP_ASYNC_RESULT* res = reinterpret_cast<const WINHTTP_ASYNC_RESULT*>(lpvStatusInformation);
Console.Error("WinHttp async function %p returned error %u", res->dwResult, res->dwError);
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
return;
}
case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
{
DbgCon.WriteLn("SendRequest complete");
if (!WinHttpReceiveResponse(hRequest, nullptr))
{
Console.Error("WinHttpReceiveResponse() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
{
DbgCon.WriteLn("Headers available");
DWORD buffer_size = sizeof(req->status_code);
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &req->status_code, &buffer_size, WINHTTP_NO_HEADER_INDEX))
{
Console.Error("WinHttpQueryHeaders() for status code failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
return;
}
buffer_size = sizeof(req->content_length);
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &req->content_length, &buffer_size,
WINHTTP_NO_HEADER_INDEX))
{
if (GetLastError() != ERROR_WINHTTP_HEADER_NOT_FOUND)
Console.Warning("WinHttpQueryHeaders() for content length failed: %u", GetLastError());
req->content_length = 0;
}
DWORD content_type_length = 0;
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX,
WINHTTP_NO_OUTPUT_BUFFER, &content_type_length, WINHTTP_NO_HEADER_INDEX) &&
GetLastError() == ERROR_INSUFFICIENT_BUFFER && content_type_length >= sizeof(content_type_length))
{
std::wstring content_type_wstring;
content_type_wstring.resize((content_type_length / sizeof(wchar_t)) - 1);
if (WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX,
content_type_wstring.data(), &content_type_length, WINHTTP_NO_HEADER_INDEX))
{
req->content_type = StringUtil::WideStringToUTF8String(content_type_wstring);
}
}
DbgCon.WriteLn("Status code %d, content-length is %u, content-type is %s", req->status_code, req->content_length,
req->content_type.c_str());
req->data.reserve(req->content_length);
req->state = Request::State::Receiving;
// start reading
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
Console.Error("WinHttpQueryDataAvailable() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
{
DWORD bytes_available;
std::memcpy(&bytes_available, lpvStatusInformation, sizeof(bytes_available));
if (bytes_available == 0)
{
// end of request
DbgCon.WriteLn("End of request '%s', %zu bytes received", req->url.c_str(), req->data.size());
req->state.store(Request::State::Complete);
return;
}
// start the transfer
DbgCon.WriteLn("%u bytes available", bytes_available);
req->io_position = static_cast<u32>(req->data.size());
req->data.resize(req->io_position + bytes_available);
if (!WinHttpReadData(hRequest, req->data.data() + req->io_position, bytes_available, nullptr) &&
GetLastError() != ERROR_IO_PENDING)
{
Console.Error("WinHttpReadData() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
{
DbgCon.WriteLn("Read of %u complete", dwStatusInformationLength);
const u32 new_size = req->io_position + dwStatusInformationLength;
pxAssertRel(new_size <= req->data.size(), "HTTP overread occurred");
req->data.resize(new_size);
req->start_time = Common::Timer::GetCurrentValue();
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
Console.Error("WinHttpQueryDataAvailable() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
return;
}
default:
// unhandled, ignore
return;
}
}
HTTPDownloader::Request* HTTPDownloaderWinHttp::InternalCreateRequest()
{
Request* req = new Request();
return req;
}
void HTTPDownloaderWinHttp::InternalPollRequests()
{
// noop - it uses windows's worker threads
}
bool HTTPDownloaderWinHttp::StartRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
std::wstring host_name;
host_name.resize(req->url.size());
req->object_name.resize(req->url.size());
URL_COMPONENTSW uc = {};
uc.dwStructSize = sizeof(uc);
uc.lpszHostName = host_name.data();
uc.dwHostNameLength = static_cast<DWORD>(host_name.size());
uc.lpszUrlPath = req->object_name.data();
uc.dwUrlPathLength = static_cast<DWORD>(req->object_name.size());
const std::wstring url_wide(StringUtil::UTF8StringToWideString(req->url));
if (!WinHttpCrackUrl(url_wide.c_str(), static_cast<DWORD>(url_wide.size()), 0, &uc))
{
Console.Error("WinHttpCrackUrl() failed: %u", GetLastError());
req->callback(HTTP_STATUS_ERROR, req->content_type, Request::Data());
delete req;
return false;
}
host_name.resize(uc.dwHostNameLength);
req->object_name.resize(uc.dwUrlPathLength);
req->hConnection = WinHttpConnect(m_hSession, host_name.c_str(), uc.nPort, 0);
if (!req->hConnection)
{
Console.Error("Failed to start HTTP request for '%s': %u", req->url.c_str(), GetLastError());
req->callback(HTTP_STATUS_ERROR, req->content_type, Request::Data());
delete req;
return false;
}
const DWORD request_flags = uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0;
req->hRequest =
WinHttpOpenRequest(req->hConnection, (req->type == HTTPDownloader::Request::Type::Post) ? L"POST" : L"GET",
req->object_name.c_str(), NULL, NULL, NULL, request_flags);
if (!req->hRequest)
{
Console.Error("WinHttpOpenRequest() failed: %u", GetLastError());
WinHttpCloseHandle(req->hConnection);
return false;
}
BOOL result;
if (req->type == HTTPDownloader::Request::Type::Post)
{
const std::wstring_view additional_headers(L"Content-Type: application/x-www-form-urlencoded\r\n");
result = WinHttpSendRequest(req->hRequest, additional_headers.data(), static_cast<DWORD>(additional_headers.size()),
req->post_data.data(), static_cast<DWORD>(req->post_data.size()),
static_cast<DWORD>(req->post_data.size()), reinterpret_cast<DWORD_PTR>(req));
}
else
{
result = WinHttpSendRequest(req->hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0,
reinterpret_cast<DWORD_PTR>(req));
}
if (!result && GetLastError() != ERROR_IO_PENDING)
{
Console.Error("WinHttpSendRequest() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
DevCon.WriteLn("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
req->start_time = Common::Timer::GetCurrentValue();
return true;
}
void HTTPDownloaderWinHttp::CloseRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
if (req->hRequest != NULL)
{
// req will be freed by the callback.
// the callback can fire immediately here if there's nothing running async, so don't touch req afterwards
WinHttpCloseHandle(req->hRequest);
return;
}
if (req->hConnection != NULL)
WinHttpCloseHandle(req->hConnection);
delete req;
}

View File

@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/HTTPDownloader.h"
#include "common/RedtapeWindows.h"
#include <winhttp.h>
class HTTPDownloaderWinHttp final : public HTTPDownloader
{
public:
HTTPDownloaderWinHttp();
~HTTPDownloaderWinHttp() override;
bool Initialize(std::string user_agent);
protected:
Request* InternalCreateRequest() override;
void InternalPollRequests() override;
bool StartRequest(HTTPDownloader::Request* request) override;
void CloseRequest(HTTPDownloader::Request* request) override;
private:
struct Request : HTTPDownloader::Request
{
std::wstring object_name;
HINTERNET hConnection = NULL;
HINTERNET hRequest = NULL;
u32 io_position = 0;
};
static void CALLBACK HTTPStatusCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus,
LPVOID lpvStatusInformation, DWORD dwStatusInformationLength);
static bool CheckCancelled(Request* request);
HINTERNET m_hSession = NULL;
};

14
common/HashCombine.h Normal file
View File

@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <functional>
#include <utility>
template <typename T, typename... Rest>
static inline void HashCombine(std::size_t& seed, const T& v, Rest&&... rest)
{
seed ^= std::hash<T>{}(v) + 0x9e3779b9u + (seed << 6) + (seed >> 2);
(HashCombine(seed, std::forward<Rest>(rest)), ...);
}

401
common/HeapArray.h Normal file
View File

@@ -0,0 +1,401 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Assertions.h"
#include <algorithm>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <type_traits>
template <typename T, std::size_t SIZE, std::size_t ALIGNMENT = 0>
class FixedHeapArray
{
public:
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using reference = T&;
using const_reference = const T&;
using pointer = T*;
using const_pointer = const T*;
using this_type = FixedHeapArray<T, SIZE>;
FixedHeapArray() { allocate(); }
FixedHeapArray(const this_type& copy)
{
allocate();
std::copy(copy.cbegin(), copy.cend(), begin());
}
FixedHeapArray(this_type&& move)
{
m_data = move.m_data;
move.m_data = nullptr;
}
~FixedHeapArray() { deallocate(); }
size_type size() const { return SIZE; }
size_type capacity() const { return SIZE; }
bool empty() const { return false; }
pointer begin() { return m_data; }
pointer end() { return m_data + SIZE; }
const_pointer data() const { return m_data; }
pointer data() { return m_data; }
const_pointer cbegin() const { return m_data; }
const_pointer cend() const { return m_data + SIZE; }
const_reference operator[](size_type index) const
{
assert(index < SIZE);
return m_data[index];
}
reference operator[](size_type index)
{
assert(index < SIZE);
return m_data[index];
}
const_reference front() const { return m_data[0]; }
const_reference back() const { return m_data[SIZE - 1]; }
reference front() { return m_data[0]; }
reference back() { return m_data[SIZE - 1]; }
void fill(const_reference value) { std::fill(begin(), end(), value); }
void swap(this_type& move) { std::swap(m_data, move.m_data); }
this_type& operator=(const this_type& rhs)
{
std::copy(begin(), end(), rhs.cbegin());
return *this;
}
this_type& operator=(this_type&& move)
{
deallocate();
m_data = move.m_data;
move.m_data = nullptr;
return *this;
}
#define RELATIONAL_OPERATOR(op) \
bool operator op(const this_type& rhs) const \
{ \
for (size_type i = 0; i < SIZE; i++) \
{ \
if (!(m_data[i] op rhs.m_data[i])) \
return false; \
} \
}
RELATIONAL_OPERATOR(==);
RELATIONAL_OPERATOR(!=);
RELATIONAL_OPERATOR(<);
RELATIONAL_OPERATOR(<=);
RELATIONAL_OPERATOR(>);
RELATIONAL_OPERATOR(>=);
#undef RELATIONAL_OPERATOR
private:
void allocate()
{
if constexpr (ALIGNMENT > 0)
{
#ifdef _MSC_VER
m_data = static_cast<T*>(_aligned_malloc(SIZE * sizeof(T), ALIGNMENT));
if (!m_data)
pxFailRel("Memory allocation failed.");
#else
if (posix_memalign(reinterpret_cast<void**>(&m_data), ALIGNMENT, SIZE * sizeof(T)) != 0)
pxFailRel("Memory allocation failed.");
#endif
}
else
{
m_data = static_cast<T*>(std::malloc(SIZE * sizeof(T)));
if (!m_data)
pxFailRel("Memory allocation failed.");
}
}
void deallocate()
{
if constexpr (ALIGNMENT > 0)
{
#ifdef _MSC_VER
_aligned_free(m_data);
#else
std::free(m_data);
#endif
}
else
{
std::free(m_data);
}
}
T* m_data;
};
template <typename T, size_t alignment = 0>
class DynamicHeapArray
{
static_assert(std::is_trivially_copyable_v<T>, "T is trivially copyable");
static_assert(std::is_standard_layout_v<T>, "T is standard layout");
public:
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using reference = T&;
using const_reference = const T&;
using pointer = T*;
using const_pointer = const T*;
using this_type = DynamicHeapArray<T>;
DynamicHeapArray()
: m_data(nullptr)
, m_size(0)
{
}
DynamicHeapArray(size_t size) { internal_resize(size, nullptr, 0); }
DynamicHeapArray(const T* begin, const T* end)
{
const size_t size = reinterpret_cast<const char*>(end) - reinterpret_cast<const char*>(begin);
if (size > 0)
{
internal_resize(size / sizeof(T), nullptr, 0);
std::memcpy(m_data, begin, size);
}
else
{
m_data = nullptr;
m_size = 0;
}
}
DynamicHeapArray(const T* begin, size_t count)
{
if (count > 0)
{
internal_resize(count, nullptr, 0);
std::memcpy(m_data, begin, sizeof(T) * count);
}
else
{
m_data = nullptr;
m_size = 0;
}
}
DynamicHeapArray(const this_type& copy)
{
if (copy.m_size > 0)
{
internal_resize(copy.m_size, nullptr, 0);
std::memcpy(m_data, copy.m_data, sizeof(T) * copy.m_size);
}
else
{
m_data = nullptr;
m_size = 0;
}
}
DynamicHeapArray(this_type&& move)
{
m_data = move.m_data;
m_size = move.m_size;
move.m_data = nullptr;
move.m_size = 0;
}
~DynamicHeapArray() { internal_deallocate(); }
size_type size() const { return m_size; }
size_type capacity() const { return m_size; }
bool empty() const { return (m_size == 0); }
pointer begin() { return m_data; }
pointer end() { return m_data + m_size; }
const_pointer data() const { return m_data; }
pointer data() { return m_data; }
const_pointer cbegin() const { return m_data; }
const_pointer cend() const { return m_data + m_size; }
const_reference operator[](size_type index) const
{
assert(index < m_size);
return m_data[index];
}
reference operator[](size_type index)
{
assert(index < m_size);
return m_data[index];
}
const_reference front() const { return m_data[0]; }
const_reference back() const { return m_data[m_size - 1]; }
reference front() { return m_data[0]; }
reference back() { return m_data[m_size - 1]; }
void fill(const_reference value) { std::fill(begin(), end(), value); }
void swap(this_type& move)
{
std::swap(m_data, move.m_data);
std::swap(m_size, move.m_size);
}
void resize(size_t new_size) { internal_resize(new_size, m_data, m_size); }
void deallocate()
{
internal_deallocate();
m_data = nullptr;
m_size = 0;
}
void assign(const T* begin, const T* end)
{
const size_t size = reinterpret_cast<const char*>(end) - reinterpret_cast<const char*>(begin);
const size_t count = size / sizeof(T);
if (count > 0)
{
if (m_size != count)
{
internal_deallocate();
internal_resize(count, nullptr, 0);
}
std::memcpy(m_data, begin, size);
}
else
{
internal_deallocate();
m_data = nullptr;
m_size = 0;
}
}
void assign(const T* begin, size_t count)
{
if (count > 0)
{
if (m_size != count)
{
internal_deallocate();
internal_resize(count, nullptr, 0);
}
std::memcpy(m_data, begin, sizeof(T) * count);
}
else
{
internal_deallocate();
m_data = nullptr;
m_size = 0;
}
}
void assign(const this_type& copy) { assign(copy.m_data, copy.m_size); }
void assign(this_type&& move)
{
internal_deallocate();
m_data = move.m_data;
m_size = move.m_size;
move.m_data = nullptr;
move.m_size = 0;
}
this_type& operator=(const this_type& rhs)
{
assign(rhs);
return *this;
}
this_type& operator=(this_type&& move)
{
assign(std::move(move));
return *this;
}
#define RELATIONAL_OPERATOR(op, size_op) \
bool operator op(const this_type& rhs) const \
{ \
if (m_size != rhs.m_size) \
return m_size size_op rhs.m_size; \
for (size_type i = 0; i < m_size; i++) \
{ \
if (!(m_data[i] op rhs.m_data[i])) \
return false; \
} \
}
RELATIONAL_OPERATOR(==, !=);
RELATIONAL_OPERATOR(!=, ==);
RELATIONAL_OPERATOR(<, <);
RELATIONAL_OPERATOR(<=, <=);
RELATIONAL_OPERATOR(>, >);
RELATIONAL_OPERATOR(>=, >=);
#undef RELATIONAL_OPERATOR
private:
void internal_resize(size_t size, T* prev_ptr, size_t prev_size)
{
if constexpr (alignment > 0)
{
#ifdef _MSC_VER
m_data = static_cast<T*>(_aligned_realloc(prev_ptr, size * sizeof(T), alignment));
if (!m_data)
pxFailRel("Memory allocation failed.");
#else
if (posix_memalign(reinterpret_cast<void**>(&m_data), alignment, size * sizeof(T)) != 0)
pxFailRel("Memory allocation failed.");
if (prev_ptr)
{
std::memcpy(m_data, prev_ptr, std::min(size, prev_size) * sizeof(T));
std::free(prev_ptr);
}
#endif
}
else
{
m_data = static_cast<T*>(std::realloc(prev_ptr, size * sizeof(T)));
if (!m_data)
pxFailRel("Memory allocation failed.");
}
m_size = size;
}
void internal_deallocate()
{
if constexpr (alignment > 0)
{
#ifdef _MSC_VER
_aligned_free(m_data);
#else
std::free(m_data);
#endif
}
else
{
std::free(m_data);
}
}
T* m_data;
size_t m_size;
};

View File

@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
/**
* Provides a map template which doesn't require heap allocations for lookups.
*/
#pragma once
#include "Pcsx2Defs.h"
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
namespace detail
{
struct transparent_string_hash
{
using is_transparent = void;
std::size_t operator()(const std::string_view& v) const { return std::hash<std::string_view>{}(v); }
std::size_t operator()(const std::string& s) const { return std::hash<std::string>{}(s); }
std::size_t operator()(const char* s) const { return operator()(std::string_view(s)); }
};
struct transparent_string_equal
{
using is_transparent = void;
bool operator()(const std::string& lhs, const std::string_view& rhs) const { return lhs == rhs; }
bool operator()(const std::string& lhs, const std::string& rhs) const { return lhs == rhs; }
bool operator()(const std::string& lhs, const char* rhs) const { return lhs == rhs; }
bool operator()(const std::string_view& lhs, const std::string& rhs) const { return lhs == rhs; }
bool operator()(const char* lhs, const std::string& rhs) const { return lhs == rhs; }
};
struct transparent_string_less
{
using is_transparent = void;
bool operator()(const std::string& lhs, const std::string_view& rhs) const { return lhs < rhs; }
bool operator()(const std::string& lhs, const std::string& rhs) const { return lhs < rhs; }
bool operator()(const std::string& lhs, const char* rhs) const { return lhs < rhs; }
bool operator()(const std::string_view& lhs, const std::string& rhs) const { return lhs < rhs; }
bool operator()(const char* lhs, const std::string& rhs) const { return lhs < rhs; }
};
} // namespace detail
template <typename ValueType>
using UnorderedStringMap =
std::unordered_map<std::string, ValueType, detail::transparent_string_hash, detail::transparent_string_equal>;
template <typename ValueType>
using UnorderedStringMultimap =
std::unordered_multimap<std::string, ValueType, detail::transparent_string_hash, detail::transparent_string_equal>;
using UnorderedStringSet =
std::unordered_set<std::string, detail::transparent_string_hash, detail::transparent_string_equal>;
using UnorderedStringMultiSet =
std::unordered_multiset<std::string, detail::transparent_string_hash, detail::transparent_string_equal>;
template <typename ValueType>
using StringMap = std::map<std::string, ValueType, detail::transparent_string_less>;
template <typename ValueType>
using StringMultiMap = std::multimap<std::string, ValueType, detail::transparent_string_less>;
using StringSet = std::set<std::string, detail::transparent_string_less>;
using StringMultiSet = std::multiset<std::string, detail::transparent_string_less>;

137
common/HostSys.cpp Normal file
View File

@@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "HostSys.h"
#include "Console.h"
#include "VectorIntrin.h"
static u32 PAUSE_TIME = 0;
static void MultiPause()
{
#ifdef _M_X86
_mm_pause();
_mm_pause();
_mm_pause();
_mm_pause();
_mm_pause();
_mm_pause();
_mm_pause();
_mm_pause();
#elif defined(_M_ARM64) && defined(_MSC_VER)
__isb(_ARM64_BARRIER_SY);
__isb(_ARM64_BARRIER_SY);
__isb(_ARM64_BARRIER_SY);
__isb(_ARM64_BARRIER_SY);
__isb(_ARM64_BARRIER_SY);
__isb(_ARM64_BARRIER_SY);
__isb(_ARM64_BARRIER_SY);
__isb(_ARM64_BARRIER_SY);
#elif defined(_M_ARM64)
__asm__ __volatile__("isb");
__asm__ __volatile__("isb");
__asm__ __volatile__("isb");
__asm__ __volatile__("isb");
__asm__ __volatile__("isb");
__asm__ __volatile__("isb");
__asm__ __volatile__("isb");
__asm__ __volatile__("isb");
#else
#error Unknown architecture.
#endif
}
static u32 MeasurePauseTime()
{
// GetCPUTicks may have resolution as low as 1µs
// One call to MultiPause could take anywhere from 20ns (fast Haswell) to 400ns (slow Skylake)
// We want a measurement of reasonable resolution, but don't want to take too long
// So start at a fairly small number and increase it if it's too fast
for (int testcnt = 64; true; testcnt *= 2)
{
u64 start = GetCPUTicks();
for (int i = 0; i < testcnt; i++)
{
MultiPause();
}
u64 time = GetCPUTicks() - start;
if (time > 100)
{
u64 nanos = (time * 1000000000) / GetTickFrequency();
return (nanos / testcnt) + 1;
}
}
}
__noinline static void UpdatePauseTime()
{
u64 wait = GetCPUTicks() + GetTickFrequency() / 100; // Wake up processor (spin for 10ms)
while (GetCPUTicks() < wait)
;
u32 pause = MeasurePauseTime();
// Take a few measurements in case something weird happens during one
// (e.g. OS interrupt)
for (int i = 0; i < 4; i++)
pause = std::min(pause, MeasurePauseTime());
PAUSE_TIME = pause;
DevCon.WriteLn("MultiPause time: %uns", pause);
}
u32 ShortSpin()
{
u32 inc = PAUSE_TIME;
if (inc == 0) [[unlikely]]
{
UpdatePauseTime();
inc = PAUSE_TIME;
}
u32 time = 0;
// Sleep for approximately 500ns
for (; time < 500; time += inc)
MultiPause();
return time;
}
static u32 GetSpinTime()
{
if (char* req = getenv("WAIT_SPIN_MICROSECONDS"))
{
return 1000 * atoi(req);
}
else
{
return 50 * 1000; // 50µs
}
}
const u32 SPIN_TIME_NS = GetSpinTime();
#ifdef __APPLE__
// https://alastairs-place.net/blog/2013/01/10/interesting-os-x-crash-report-tidbits/
// https://opensource.apple.com/source/WebKit2/WebKit2-7608.3.10.0.3/Platform/spi/Cocoa/CrashReporterClientSPI.h.auto.html
struct crash_info_t
{
u64 version;
u64 message;
u64 signature;
u64 backtrace;
u64 message2;
u64 reserved;
u64 reserved2;
};
#define CRASH_ANNOTATION __attribute__((used, section("__DATA,__crash_info")))
#define CRASH_VERSION 4
extern "C" crash_info_t gCRAnnotations CRASH_ANNOTATION = { CRASH_VERSION };
#endif
void AbortWithMessage(const char* msg)
{
#ifdef __APPLE__
gCRAnnotations.message = reinterpret_cast<size_t>(msg);
// Some macOS's seem to have issues displaying non-static `message`s, so throw it in here too
gCRAnnotations.backtrace = gCRAnnotations.message;
#endif
abort();
}

207
common/HostSys.h Normal file
View File

@@ -0,0 +1,207 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Defs.h"
#include <atomic>
#include <functional>
#include <map>
#include <memory>
#include <string>
class Error;
// --------------------------------------------------------------------------------------
// PageProtectionMode
// --------------------------------------------------------------------------------------
class PageProtectionMode
{
protected:
bool m_read = false;
bool m_write = false;
bool m_exec = false;
public:
__fi constexpr PageProtectionMode() = default;
__fi constexpr PageProtectionMode& Read(bool allow = true)
{
m_read = allow;
return *this;
}
__fi constexpr PageProtectionMode& Write(bool allow = true)
{
m_write = allow;
return *this;
}
__fi constexpr PageProtectionMode& Execute(bool allow = true)
{
m_exec = allow;
return *this;
}
__fi constexpr PageProtectionMode& All(bool allow = true)
{
m_read = m_write = m_exec = allow;
return *this;
}
__fi constexpr bool CanRead() const { return m_read; }
__fi constexpr bool CanWrite() const { return m_write; }
__fi constexpr bool CanExecute() const { return m_exec && m_read; }
__fi constexpr bool IsNone() const { return !m_read && !m_write; }
};
static __fi PageProtectionMode PageAccess_None()
{
return PageProtectionMode();
}
static __fi PageProtectionMode PageAccess_ReadOnly()
{
return PageProtectionMode().Read();
}
static __fi PageProtectionMode PageAccess_WriteOnly()
{
return PageProtectionMode().Write();
}
static __fi PageProtectionMode PageAccess_ReadWrite()
{
return PageAccess_ReadOnly().Write();
}
static __fi PageProtectionMode PageAccess_ExecOnly()
{
return PageAccess_ReadOnly().Execute();
}
static __fi PageProtectionMode PageAccess_Any()
{
return PageProtectionMode().All();
}
// --------------------------------------------------------------------------------------
// HostSys
// --------------------------------------------------------------------------------------
namespace HostSys
{
// Maps a block of memory for use as a recompiled code buffer.
// Returns NULL on allocation failure.
extern void* Mmap(void* base, size_t size, const PageProtectionMode& mode);
// Unmaps a block allocated by SysMmap
extern void Munmap(void* base, size_t size);
extern void MemProtect(void* baseaddr, size_t size, const PageProtectionMode& mode);
extern std::string GetFileMappingName(const char* prefix);
extern void* CreateSharedMemory(const char* name, size_t size);
extern void DestroySharedMemory(void* ptr);
extern void* MapSharedMemory(void* handle, size_t offset, void* baseaddr, size_t size, const PageProtectionMode& mode);
extern void UnmapSharedMemory(void* baseaddr, size_t size);
/// JIT write protect for Apple Silicon. Needs to be called prior to writing to any RWX pages.
#if !defined(__APPLE__) || !defined(_M_ARM64)
// clang-format -off
[[maybe_unused]] __fi static void BeginCodeWrite() {}
[[maybe_unused]] __fi static void EndCodeWrite() {}
// clang-format on
#else
void BeginCodeWrite();
void EndCodeWrite();
#endif
/// Flushes the instruction cache on the host for the specified range.
/// Only needed on ARM64, X86 has coherent D/I cache.
#ifdef _M_X86
[[maybe_unused]] __fi static void FlushInstructionCache(void* address, u32 size) {}
#else
void FlushInstructionCache(void* address, u32 size);
#endif
/// Returns the size of pages for the current host.
size_t GetRuntimePageSize();
/// Returns the size of a cache line for the current host.
size_t GetRuntimeCacheLineSize();
} // namespace HostSys
namespace PageFaultHandler
{
enum class HandlerResult
{
ContinueExecution,
ExecuteNextHandler,
};
HandlerResult HandlePageFault(void* exception_pc, void* fault_address, bool is_write);
bool Install(Error* error = nullptr);
} // namespace PageFaultHandler
class SharedMemoryMappingArea
{
public:
static std::unique_ptr<SharedMemoryMappingArea> Create(size_t size);
~SharedMemoryMappingArea();
__fi size_t GetSize() const { return m_size; }
__fi size_t GetNumPages() const { return m_num_pages; }
__fi u8* BasePointer() const { return m_base_ptr; }
__fi u8* OffsetPointer(size_t offset) const { return m_base_ptr + offset; }
__fi u8* PagePointer(size_t page) const { return m_base_ptr + __pagesize * page; }
u8* Map(void* file_handle, size_t file_offset, void* map_base, size_t map_size, const PageProtectionMode& mode);
bool Unmap(void* map_base, size_t map_size);
private:
SharedMemoryMappingArea(u8* base_ptr, size_t size, size_t num_pages);
u8* m_base_ptr;
size_t m_size;
size_t m_num_pages;
size_t m_num_mappings = 0;
#ifdef _WIN32
using PlaceholderMap = std::map<size_t, size_t>;
PlaceholderMap::iterator FindPlaceholder(size_t page);
PlaceholderMap m_placeholder_ranges;
#endif
};
extern u64 GetTickFrequency();
extern u64 GetCPUTicks();
extern u64 GetPhysicalMemory();
extern u64 GetAvailablePhysicalMemory();
/// Spin for a short period of time (call while spinning waiting for a lock)
/// Returns the approximate number of ns that passed
extern u32 ShortSpin();
/// Number of ns to spin for before sleeping a thread
extern const u32 SPIN_TIME_NS;
/// Like C abort() but adds the given message to the crashlog
[[noreturn]] void AbortWithMessage(const char* msg);
extern std::string GetOSVersionString();
namespace Common
{
/// Enables or disables the screen saver from starting.
bool InhibitScreensaver(bool inhibit);
/// Abstracts platform-specific code for asynchronously playing a sound.
/// On Windows, this will use PlaySound(). On Linux, it will shell out to aplay. On MacOS, it uses NSSound.
bool PlaySoundAsync(const char* path);
void SetMousePosition(int x, int y);
bool AttachMousePositionCb(std::function<void(int,int)> cb);
void DetachMousePositionCb();
} // namespace Common

736
common/Image.cpp Normal file
View File

@@ -0,0 +1,736 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Image.h"
#include "FileSystem.h"
#include "Console.h"
#include "Path.h"
#include "ScopedGuard.h"
#include "StringUtil.h"
#include <common/FastJmp.h>
#include <jpeglib.h>
#include <png.h>
#include <webp/decode.h>
#include <webp/encode.h>
// Compute the address of a base type given a field offset.
#define BASE_FROM_RECORD_FIELD(ptr, base_type, field) ((base_type*)(((char*)ptr) - offsetof(base_type, field)))
static bool PNGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
static bool PNGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality);
static bool PNGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp);
static bool PNGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, u8 quality);
static bool JPEGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
static bool JPEGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality);
static bool JPEGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp);
static bool JPEGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, u8 quality);
static bool WebPBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size);
static bool WebPBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality);
static bool WebPFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp);
static bool WebPFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, u8 quality);
struct FormatHandler
{
const char* extension;
bool (*buffer_loader)(RGBA8Image*, const void*, size_t);
bool (*buffer_saver)(const RGBA8Image&, std::vector<u8>*, u8);
bool (*file_loader)(RGBA8Image*, const char*, std::FILE*);
bool (*file_saver)(const RGBA8Image&, const char*, std::FILE*, u8);
};
static constexpr FormatHandler s_format_handlers[] = {
{"png", PNGBufferLoader, PNGBufferSaver, PNGFileLoader, PNGFileSaver},
{"jpg", JPEGBufferLoader, JPEGBufferSaver, JPEGFileLoader, JPEGFileSaver},
{"jpeg", JPEGBufferLoader, JPEGBufferSaver, JPEGFileLoader, JPEGFileSaver},
{"webp", WebPBufferLoader, WebPBufferSaver, WebPFileLoader, WebPFileSaver},
};
static const FormatHandler* GetFormatHandler(const std::string_view extension)
{
for (const FormatHandler& handler : s_format_handlers)
{
if (StringUtil::Strncasecmp(extension.data(), handler.extension, extension.size()) == 0)
return &handler;
}
return nullptr;
}
RGBA8Image::RGBA8Image() = default;
RGBA8Image::RGBA8Image(const RGBA8Image& copy)
: Image(copy)
{
}
RGBA8Image::RGBA8Image(u32 width, u32 height, const u32* pixels)
: Image(width, height, pixels)
{
}
RGBA8Image::RGBA8Image(RGBA8Image&& move)
: Image(move)
{
}
RGBA8Image::RGBA8Image(u32 width, u32 height)
: Image(width, height)
{
}
RGBA8Image::RGBA8Image(u32 width, u32 height, std::vector<u32> pixels)
: Image(width, height, std::move(pixels))
{
}
RGBA8Image& RGBA8Image::operator=(const RGBA8Image& copy)
{
Image<u32>::operator=(copy);
return *this;
}
RGBA8Image& RGBA8Image::operator=(RGBA8Image&& move)
{
Image<u32>::operator=(move);
return *this;
}
bool RGBA8Image::LoadFromFile(const char* filename)
{
auto fp = FileSystem::OpenManagedCFile(filename, "rb");
if (!fp)
return false;
return LoadFromFile(filename, fp.get());
}
bool RGBA8Image::SaveToFile(const char* filename, u8 quality) const
{
auto fp = FileSystem::OpenManagedCFile(filename, "wb");
if (!fp)
return false;
if (SaveToFile(filename, fp.get(), quality))
return true;
// save failed
fp.reset();
FileSystem::DeleteFilePath(filename);
return false;
}
bool RGBA8Image::LoadFromFile(const char* filename, std::FILE* fp)
{
const std::string_view extension(Path::GetExtension(filename));
const FormatHandler* handler = GetFormatHandler(extension);
if (!handler || !handler->file_loader)
{
Console.ErrorFmt("Unknown extension '{}'", extension);
return false;
}
return handler->file_loader(this, filename, fp);
}
bool RGBA8Image::LoadFromBuffer(const char* filename, const void* buffer, size_t buffer_size)
{
const std::string_view extension(Path::GetExtension(filename));
const FormatHandler* handler = GetFormatHandler(extension);
if (!handler || !handler->buffer_loader)
{
Console.ErrorFmt("Unknown extension '{}'", extension);
return false;
}
return handler->buffer_loader(this, buffer, buffer_size);
}
bool RGBA8Image::SaveToFile(const char* filename, std::FILE* fp, u8 quality) const
{
const std::string_view extension(Path::GetExtension(filename));
const FormatHandler* handler = GetFormatHandler(extension);
if (!handler || !handler->file_saver)
{
Console.ErrorFmt("Unknown extension '{}'", extension);
return false;
}
if (!handler->file_saver(*this, filename, fp, quality))
return false;
return (std::fflush(fp) == 0);
}
std::optional<std::vector<u8>> RGBA8Image::SaveToBuffer(const char* filename, u8 quality) const
{
std::optional<std::vector<u8>> ret;
const std::string_view extension(Path::GetExtension(filename));
const FormatHandler* handler = GetFormatHandler(extension);
if (!handler || !handler->file_saver)
{
Console.ErrorFmt("Unknown extension '{}'", extension);
return ret;
}
ret = std::vector<u8>();
if (!handler->buffer_saver(*this, &ret.value(), quality))
ret.reset();
return ret;
}
static bool PNGCommonLoader(RGBA8Image* image, png_structp png_ptr, png_infop info_ptr, std::vector<u32>& new_data,
std::vector<png_bytep>& row_pointers)
{
png_read_info(png_ptr, info_ptr);
const u32 width = png_get_image_width(png_ptr, info_ptr);
const u32 height = png_get_image_height(png_ptr, info_ptr);
const png_byte color_type = png_get_color_type(png_ptr, info_ptr);
const png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
// Read any color_type into 8bit depth, RGBA format.
// See http://www.libpng.org/pub/png/libpng-manual.txt
if (bit_depth == 16)
png_set_strip_16(png_ptr);
if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(png_ptr);
// PNG_COLOR_TYPE_GRAY_ALPHA is always 8 or 16bit depth.
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
png_set_expand_gray_1_2_4_to_8(png_ptr);
if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png_ptr);
// These color_type don't have an alpha channel then fill it with 0xff.
if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)
png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png_ptr);
png_read_update_info(png_ptr, info_ptr);
new_data.resize(width * height);
row_pointers.reserve(height);
for (u32 y = 0; y < height; y++)
row_pointers.push_back(reinterpret_cast<png_bytep>(new_data.data() + y * width));
png_read_image(png_ptr, row_pointers.data());
image->SetPixels(width, height, std::move(new_data));
return true;
}
bool PNGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp)
{
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_ptr)
return false;
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
return false;
}
ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
std::vector<u32> new_data;
std::vector<png_bytep> row_pointers;
if (setjmp(png_jmpbuf(png_ptr)))
return false;
png_set_read_fn(png_ptr, fp, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
std::FILE* fp = static_cast<std::FILE*>(png_get_io_ptr(png_ptr));
if (std::fread(data_ptr, size, 1, fp) != 1)
png_error(png_ptr, "Read error");
});
return PNGCommonLoader(image, png_ptr, info_ptr, new_data, row_pointers);
}
bool PNGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
{
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_ptr)
return false;
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
return false;
}
ScopedGuard cleanup([&png_ptr, &info_ptr]() { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); });
std::vector<u32> new_data;
std::vector<png_bytep> row_pointers;
if (setjmp(png_jmpbuf(png_ptr)))
return false;
struct IOData
{
const u8* buffer;
size_t buffer_size;
size_t buffer_pos;
};
IOData data = {static_cast<const u8*>(buffer), buffer_size, 0};
png_set_read_fn(png_ptr, &data, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
IOData* data = static_cast<IOData*>(png_get_io_ptr(png_ptr));
const size_t read_size = std::min<size_t>(data->buffer_size - data->buffer_pos, size);
if (read_size > 0)
{
std::memcpy(data_ptr, data->buffer + data->buffer_pos, read_size);
data->buffer_pos += read_size;
}
});
return PNGCommonLoader(image, png_ptr, info_ptr, new_data, row_pointers);
}
static void PNGSaveCommon(const RGBA8Image& image, png_structp png_ptr, png_infop info_ptr, u8 quality)
{
png_set_compression_level(png_ptr, std::clamp(quality / 10, 0, 9));
png_set_IHDR(png_ptr, info_ptr, image.GetWidth(), image.GetHeight(), 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png_ptr, info_ptr);
for (u32 y = 0; y < image.GetHeight(); ++y)
png_write_row(png_ptr, (png_bytep)image.GetRowPixels(y));
png_write_end(png_ptr, nullptr);
}
bool PNGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, u8 quality)
{
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
png_infop info_ptr = nullptr;
if (!png_ptr)
return false;
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
if (png_ptr)
png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
});
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
return false;
if (setjmp(png_jmpbuf(png_ptr)))
return false;
png_set_write_fn(
png_ptr, fp,
[](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
if (std::fwrite(data_ptr, size, 1, static_cast<std::FILE*>(png_get_io_ptr(png_ptr))) != 1)
png_error(png_ptr, "file write error");
},
[](png_structp png_ptr) {});
PNGSaveCommon(image, png_ptr, info_ptr, quality);
return true;
}
bool PNGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality)
{
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
png_infop info_ptr = nullptr;
if (!png_ptr)
return false;
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
if (png_ptr)
png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
});
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
return false;
buffer->reserve(image.GetWidth() * image.GetHeight() * 2);
if (setjmp(png_jmpbuf(png_ptr)))
return false;
png_set_write_fn(
png_ptr, buffer,
[](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
std::vector<u8>* buffer = static_cast<std::vector<u8>*>(png_get_io_ptr(png_ptr));
const size_t old_pos = buffer->size();
buffer->resize(old_pos + size);
std::memcpy(buffer->data() + old_pos, data_ptr, size);
},
[](png_structp png_ptr) {});
PNGSaveCommon(image, png_ptr, info_ptr, quality);
return true;
}
namespace
{
struct JPEGErrorHandler
{
jpeg_error_mgr err;
fastjmp_buf jbuf;
JPEGErrorHandler()
{
jpeg_std_error(&err);
err.error_exit = &ErrorExit;
}
static void ErrorExit(j_common_ptr cinfo)
{
JPEGErrorHandler* eh = (JPEGErrorHandler*)cinfo->err;
char msg[JMSG_LENGTH_MAX];
eh->err.format_message(cinfo, msg);
Console.ErrorFmt("libjpeg fatal error: {}", msg);
fastjmp_jmp(&eh->jbuf, 1);
}
};
} // namespace
template <typename T>
static bool WrapJPEGDecompress(RGBA8Image* image, T setup_func)
{
std::vector<u8> scanline;
jpeg_decompress_struct info = {};
// NOTE: Be **very** careful not to allocate memory after calling this function.
// It won't get freed, because fastjmp does not unwind the stack.
JPEGErrorHandler errhandler;
if (fastjmp_set(&errhandler.jbuf) != 0)
{
jpeg_destroy_decompress(&info);
return false;
}
info.err = &errhandler.err;
jpeg_create_decompress(&info);
setup_func(info);
const int herr = jpeg_read_header(&info, TRUE);
if (herr != JPEG_HEADER_OK)
{
Console.ErrorFmt("jpeg_read_header() returned {}", herr);
return false;
}
if (info.image_width == 0 || info.image_height == 0 || info.num_components < 3)
{
Console.ErrorFmt("Invalid image dimensions: {}x{}x{}", info.image_width, info.image_height, info.num_components);
return false;
}
info.out_color_space = JCS_RGB;
info.out_color_components = 3;
if (!jpeg_start_decompress(&info))
{
Console.ErrorFmt("jpeg_start_decompress() returned failure");
return false;
}
image->SetSize(info.image_width, info.image_height);
scanline.resize(info.image_width * 3);
u8* scanline_buffer[1] = {scanline.data()};
bool result = true;
for (u32 y = 0; y < info.image_height; y++)
{
if (jpeg_read_scanlines(&info, scanline_buffer, 1) != 1)
{
Console.ErrorFmt("jpeg_read_scanlines() failed at row {}", y);
result = false;
break;
}
// RGB -> RGBA
const u8* src_ptr = scanline.data();
u32* dst_ptr = image->GetRowPixels(y);
for (u32 x = 0; x < info.image_width; x++)
{
*(dst_ptr++) = (static_cast<u32>(src_ptr[0]) | (static_cast<u32>(src_ptr[1]) << 8) | (static_cast<u32>(src_ptr[2]) << 16) | 0xFF000000u);
src_ptr += 3;
}
}
jpeg_finish_decompress(&info);
jpeg_destroy_decompress(&info);
return result;
}
bool JPEGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
{
return WrapJPEGDecompress(image, [buffer, buffer_size](jpeg_decompress_struct& info) {
jpeg_mem_src(&info, static_cast<const unsigned char*>(buffer), buffer_size);
});
}
bool JPEGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp)
{
static constexpr u32 BUFFER_SIZE = 16384;
struct FileCallback
{
jpeg_source_mgr mgr;
std::FILE* fp;
std::unique_ptr<u8[]> buffer;
bool end_of_file;
};
FileCallback cb = {
.mgr = {
.init_source = [](j_decompress_ptr cinfo) {},
.fill_input_buffer = [](j_decompress_ptr cinfo) -> boolean {
FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->src, FileCallback, mgr);
cb->mgr.next_input_byte = cb->buffer.get();
if (cb->end_of_file)
{
cb->buffer[0] = 0xFF;
cb->buffer[1] = JPEG_EOI;
cb->mgr.bytes_in_buffer = 2;
return TRUE;
}
const size_t r = std::fread(cb->buffer.get(), 1, BUFFER_SIZE, cb->fp);
cb->end_of_file |= (std::feof(cb->fp) != 0);
cb->mgr.bytes_in_buffer = r;
return TRUE;
},
.skip_input_data =
[](j_decompress_ptr cinfo, long num_bytes) {
FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->src, FileCallback, mgr);
const size_t skip_in_buffer = std::min<size_t>(cb->mgr.bytes_in_buffer, static_cast<size_t>(num_bytes));
cb->mgr.next_input_byte += skip_in_buffer;
cb->mgr.bytes_in_buffer -= skip_in_buffer;
const size_t seek_cur = static_cast<size_t>(num_bytes) - skip_in_buffer;
if (seek_cur > 0)
{
if (FileSystem::FSeek64(cb->fp, static_cast<size_t>(seek_cur), SEEK_CUR) != 0)
{
cb->end_of_file = true;
return;
}
}
},
.resync_to_restart = jpeg_resync_to_restart,
.term_source = [](j_decompress_ptr cinfo) {},
},
.fp = fp,
.buffer = std::make_unique<u8[]>(BUFFER_SIZE),
.end_of_file = false,
};
return WrapJPEGDecompress(image, [&cb](jpeg_decompress_struct& info) { info.src = &cb.mgr; });
}
template <typename T>
static bool WrapJPEGCompress(const RGBA8Image& image, u8 quality, T setup_func)
{
std::vector<u8> scanline;
jpeg_compress_struct info = {};
// NOTE: Be **very** careful not to allocate memory after calling this function.
// It won't get freed, because fastjmp does not unwind the stack.
JPEGErrorHandler errhandler;
if (fastjmp_set(&errhandler.jbuf) != 0)
{
jpeg_destroy_compress(&info);
return false;
}
info.err = &errhandler.err;
jpeg_create_compress(&info);
setup_func(info);
info.image_width = image.GetWidth();
info.image_height = image.GetHeight();
info.in_color_space = JCS_RGB;
info.input_components = 3;
jpeg_set_defaults(&info);
jpeg_set_quality(&info, quality, TRUE);
jpeg_start_compress(&info, TRUE);
scanline.resize(image.GetWidth() * 3);
u8* scanline_buffer[1] = {scanline.data()};
bool result = true;
for (u32 y = 0; y < info.image_height; y++)
{
// RGBA -> RGB
u8* dst_ptr = scanline.data();
const u32* src_ptr = image.GetRowPixels(y);
for (u32 x = 0; x < info.image_width; x++)
{
const u32 rgba = *(src_ptr++);
*(dst_ptr++) = static_cast<u8>(rgba);
*(dst_ptr++) = static_cast<u8>(rgba >> 8);
*(dst_ptr++) = static_cast<u8>(rgba >> 16);
}
if (jpeg_write_scanlines(&info, scanline_buffer, 1) != 1)
{
Console.ErrorFmt("jpeg_write_scanlines() failed at row {}", y);
result = false;
break;
}
}
jpeg_finish_compress(&info);
jpeg_destroy_compress(&info);
return result;
}
bool JPEGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality)
{
// give enough space to avoid reallocs
buffer->resize(image.GetWidth() * image.GetHeight() * 2);
struct MemCallback
{
jpeg_destination_mgr mgr;
std::vector<u8>* buffer;
size_t buffer_used;
};
MemCallback cb;
cb.buffer = buffer;
cb.buffer_used = 0;
cb.mgr.next_output_byte = buffer->data();
cb.mgr.free_in_buffer = buffer->size();
cb.mgr.init_destination = [](j_compress_ptr cinfo) {};
cb.mgr.empty_output_buffer = [](j_compress_ptr cinfo) -> boolean {
MemCallback* cb = (MemCallback*)cinfo->dest;
// double size
cb->buffer_used = cb->buffer->size();
cb->buffer->resize(cb->buffer->size() * 2);
cb->mgr.next_output_byte = cb->buffer->data() + cb->buffer_used;
cb->mgr.free_in_buffer = cb->buffer->size() - cb->buffer_used;
return TRUE;
};
cb.mgr.term_destination = [](j_compress_ptr cinfo) {
MemCallback* cb = (MemCallback*)cinfo->dest;
// get final size
cb->buffer->resize(cb->buffer->size() - cb->mgr.free_in_buffer);
};
return WrapJPEGCompress(image, quality, [&cb](jpeg_compress_struct& info) { info.dest = &cb.mgr; });
}
bool JPEGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, u8 quality)
{
static constexpr u32 BUFFER_SIZE = 16384;
struct FileCallback
{
jpeg_destination_mgr mgr;
std::FILE* fp;
std::unique_ptr<u8[]> buffer;
bool write_error;
};
FileCallback cb = {
.mgr = {
.init_destination =
[](j_compress_ptr cinfo) {
FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
cb->mgr.next_output_byte = cb->buffer.get();
cb->mgr.free_in_buffer = BUFFER_SIZE;
},
.empty_output_buffer = [](j_compress_ptr cinfo) -> boolean {
FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
if (!cb->write_error)
cb->write_error |= (std::fwrite(cb->buffer.get(), 1, BUFFER_SIZE, cb->fp) != BUFFER_SIZE);
cb->mgr.next_output_byte = cb->buffer.get();
cb->mgr.free_in_buffer = BUFFER_SIZE;
return TRUE;
},
.term_destination =
[](j_compress_ptr cinfo) {
FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
const size_t left = BUFFER_SIZE - cb->mgr.free_in_buffer;
if (left > 0 && !cb->write_error)
cb->write_error |= (std::fwrite(cb->buffer.get(), 1, left, cb->fp) != left);
},
},
.fp = fp,
.buffer = std::make_unique<u8[]>(BUFFER_SIZE),
.write_error = false,
};
return (WrapJPEGCompress(image, quality, [&cb](jpeg_compress_struct& info) { info.dest = &cb.mgr; }) &&
!cb.write_error);
}
bool WebPBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
{
int width, height;
if (!WebPGetInfo(static_cast<const u8*>(buffer), buffer_size, &width, &height) || width <= 0 || height <= 0)
{
Console.Error("WebPGetInfo() failed");
return false;
}
std::vector<u32> pixels;
pixels.resize(static_cast<u32>(width) * static_cast<u32>(height));
if (!WebPDecodeRGBAInto(static_cast<const u8*>(buffer), buffer_size, reinterpret_cast<u8*>(pixels.data()),
sizeof(u32) * pixels.size(), sizeof(u32) * static_cast<u32>(width)))
{
Console.Error("WebPDecodeRGBAInto() failed");
return false;
}
image->SetPixels(static_cast<u32>(width), static_cast<u32>(height), std::move(pixels));
return true;
}
bool WebPBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 quality)
{
u8* encoded_data;
const size_t encoded_size =
WebPEncodeRGBA(reinterpret_cast<const u8*>(image.GetPixels()), image.GetWidth(), image.GetHeight(),
image.GetPitch(), static_cast<float>(quality), &encoded_data);
if (encoded_size == 0)
return false;
buffer->resize(encoded_size);
std::memcpy(buffer->data(), encoded_data, encoded_size);
WebPFree(encoded_data);
return true;
}
bool WebPFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp)
{
std::optional<std::vector<u8>> data = FileSystem::ReadBinaryFile(fp);
if (!data.has_value())
return false;
return WebPBufferLoader(image, data->data(), data->size());
}
bool WebPFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, u8 quality)
{
std::vector<u8> buffer;
if (!WebPBufferSaver(image, &buffer, quality))
return false;
return (std::fwrite(buffer.data(), buffer.size(), 1, fp) == 1);
}

133
common/Image.h Normal file
View File

@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <optional>
#include <vector>
template <typename PixelType>
class Image
{
public:
Image() = default;
Image(u32 width, u32 height) { SetSize(width, height); }
Image(u32 width, u32 height, const PixelType* pixels) { SetPixels(width, height, pixels); }
Image(u32 width, u32 height, std::vector<PixelType> pixels) { SetPixels(width, height, std::move(pixels)); }
Image(const Image& copy)
{
m_width = copy.m_width;
m_height = copy.m_height;
m_pixels = copy.m_pixels;
}
Image(Image&& move)
{
m_width = move.m_width;
m_height = move.m_height;
m_pixels = std::move(move.m_pixels);
move.m_width = 0;
move.m_height = 0;
}
Image& operator=(const Image& copy)
{
m_width = copy.m_width;
m_height = copy.m_height;
m_pixels = copy.m_pixels;
return *this;
}
Image& operator=(Image&& move)
{
m_width = move.m_width;
m_height = move.m_height;
m_pixels = std::move(move.m_pixels);
move.m_width = 0;
move.m_height = 0;
return *this;
}
__fi bool IsValid() const { return (m_width > 0 && m_height > 0); }
__fi u32 GetWidth() const { return m_width; }
__fi u32 GetHeight() const { return m_height; }
__fi u32 GetPitch() const { return (sizeof(PixelType) * m_width); }
__fi const PixelType* GetPixels() const { return m_pixels.data(); }
__fi PixelType* GetPixels() { return m_pixels.data(); }
__fi const PixelType* GetRowPixels(u32 y) const { return &m_pixels[y * m_width]; }
__fi PixelType* GetRowPixels(u32 y) { return &m_pixels[y * m_width]; }
__fi void SetPixel(u32 x, u32 y, PixelType pixel) { m_pixels[y * m_width + x] = pixel; }
__fi PixelType GetPixel(u32 x, u32 y) const { return m_pixels[y * m_width + x]; }
void Clear(PixelType fill_value = static_cast<PixelType>(0))
{
std::fill(m_pixels.begin(), m_pixels.end(), fill_value);
}
void Invalidate()
{
m_width = 0;
m_height = 0;
m_pixels.clear();
}
void SetSize(u32 new_width, u32 new_height, PixelType fill_value = static_cast<PixelType>(0))
{
m_width = new_width;
m_height = new_height;
m_pixels.resize(new_width * new_height);
Clear(fill_value);
}
void SetPixels(u32 width, u32 height, const PixelType* pixels)
{
m_width = width;
m_height = height;
m_pixels.resize(width * height);
std::memcpy(m_pixels.data(), pixels, width * height * sizeof(PixelType));
}
void SetPixels(u32 width, u32 height, std::vector<PixelType> pixels)
{
m_width = width;
m_height = height;
m_pixels = std::move(pixels);
}
std::vector<PixelType> TakePixels()
{
m_width = 0;
m_height = 0;
return std::move(m_pixels);
}
protected:
u32 m_width = 0;
u32 m_height = 0;
std::vector<PixelType> m_pixels;
};
class RGBA8Image : public Image<u32>
{
public:
static constexpr u8 DEFAULT_SAVE_QUALITY = 85;
RGBA8Image();
RGBA8Image(u32 width, u32 height);
RGBA8Image(u32 width, u32 height, const u32* pixels);
RGBA8Image(u32 width, u32 height, std::vector<u32> pixels);
RGBA8Image(const RGBA8Image& copy);
RGBA8Image(RGBA8Image&& move);
RGBA8Image& operator=(const RGBA8Image& copy);
RGBA8Image& operator=(RGBA8Image&& move);
bool LoadFromFile(const char* filename);
bool LoadFromFile(const char* filename, std::FILE* fp);
bool LoadFromBuffer(const char* filename, const void* buffer, size_t buffer_size);
bool SaveToFile(const char* filename, u8 quality = DEFAULT_SAVE_QUALITY) const;
bool SaveToFile(const char* filename, std::FILE* fp, u8 quality = DEFAULT_SAVE_QUALITY) const;
std::optional<std::vector<u8>> SaveToBuffer(const char* filename, u8 quality = DEFAULT_SAVE_QUALITY) const;
};

124
common/LRUCache.h Normal file
View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "HeterogeneousContainers.h"
#include <cstdint>
#include <map>
template <class K, class V>
class LRUCache
{
using CounterType = std::uint64_t;
struct Item
{
V value;
CounterType last_access;
};
using MapType = std::conditional_t<std::is_same_v<K, std::string>, StringMap<Item>, std::map<K, Item>>;
public:
LRUCache(std::size_t max_capacity = 16, bool manual_evict = false)
: m_max_capacity(max_capacity)
, m_manual_evict(manual_evict)
{
}
~LRUCache() = default;
std::size_t GetSize() const { return m_items.size(); }
std::size_t GetMaxCapacity() const { return m_max_capacity; }
void Clear() { m_items.clear(); }
void SetMaxCapacity(std::size_t capacity)
{
m_max_capacity = capacity;
if (m_items.size() > m_max_capacity)
Evict(m_items.size() - m_max_capacity);
}
template <typename KeyT>
V* Lookup(const KeyT& key)
{
auto iter = m_items.find(key);
if (iter == m_items.end())
return nullptr;
iter->second.last_access = ++m_last_counter;
return &iter->second.value;
}
V* Insert(K key, V value)
{
ShrinkForNewItem();
auto iter = m_items.find(key);
if (iter != m_items.end())
{
iter->second.value = std::move(value);
iter->second.last_access = ++m_last_counter;
return &iter->second.value;
}
else
{
Item it;
it.last_access = ++m_last_counter;
it.value = std::move(value);
auto ip = m_items.emplace(std::move(key), std::move(it));
return &ip.first->second.value;
}
}
void Evict(std::size_t count = 1)
{
while (!m_items.empty() && count > 0)
{
typename MapType::iterator lowest = m_items.end();
for (auto iter = m_items.begin(); iter != m_items.end(); ++iter)
{
if (lowest == m_items.end() || iter->second.last_access < lowest->second.last_access)
lowest = iter;
}
m_items.erase(lowest);
count--;
}
}
template <typename KeyT>
bool Remove(const KeyT& key)
{
auto iter = m_items.find(key);
if (iter == m_items.end())
return false;
m_items.erase(iter);
return true;
}
void SetManualEvict(bool block)
{
m_manual_evict = block;
if (!m_manual_evict)
ManualEvict();
}
void ManualEvict()
{
// evict if we went over
while (m_items.size() > m_max_capacity)
Evict(m_items.size() - m_max_capacity);
}
private:
void ShrinkForNewItem()
{
if (m_items.size() < m_max_capacity)
return;
Evict(m_items.size() - (m_max_capacity - 1));
}
MapType m_items;
CounterType m_last_counter = 0;
std::size_t m_max_capacity = 0;
bool m_manual_evict = false;
};

372
common/Linux/LnxHostSys.cpp Normal file
View 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
View 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
View 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
}

210
common/MD5Digest.cpp Normal file
View File

@@ -0,0 +1,210 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "MD5Digest.h"
#include <cstring>
// mostly based on this implementation (public domain): http://www.fourmilab.ch/md5/
/* The four core functions - F1 is optimized somewhat */
/* #define F1(x, y, z) (x & y | ~x & z) */
#define F1(x, y, z) (z ^ (x & (y ^ z)))
#define F2(x, y, z) F1(z, x, y)
#define F3(x, y, z) (x ^ y ^ z)
#define F4(x, y, z) (y ^ (x | ~z))
/* This is the central step in the MD5 algorithm. */
#define MD5STEP(f, w, x, y, z, data, s) (w += f(x, y, z) + data, w = w << s | w >> (32 - s), w += x)
/*
* The core of the MD5 algorithm, this alters an existing MD5 hash to
* reflect the addition of 16 longwords of new data. MD5Update blocks
* the data and converts bytes into longwords for this routine.
*/
static void MD5Transform(u32 buf[4], u32 in[16])
{
u32 a, b, c, d;
a = buf[0];
b = buf[1];
c = buf[2];
d = buf[3];
MD5STEP(F1, a, b, c, d, in[0] + 0xd76aa478, 7);
MD5STEP(F1, d, a, b, c, in[1] + 0xe8c7b756, 12);
MD5STEP(F1, c, d, a, b, in[2] + 0x242070db, 17);
MD5STEP(F1, b, c, d, a, in[3] + 0xc1bdceee, 22);
MD5STEP(F1, a, b, c, d, in[4] + 0xf57c0faf, 7);
MD5STEP(F1, d, a, b, c, in[5] + 0x4787c62a, 12);
MD5STEP(F1, c, d, a, b, in[6] + 0xa8304613, 17);
MD5STEP(F1, b, c, d, a, in[7] + 0xfd469501, 22);
MD5STEP(F1, a, b, c, d, in[8] + 0x698098d8, 7);
MD5STEP(F1, d, a, b, c, in[9] + 0x8b44f7af, 12);
MD5STEP(F1, c, d, a, b, in[10] + 0xffff5bb1, 17);
MD5STEP(F1, b, c, d, a, in[11] + 0x895cd7be, 22);
MD5STEP(F1, a, b, c, d, in[12] + 0x6b901122, 7);
MD5STEP(F1, d, a, b, c, in[13] + 0xfd987193, 12);
MD5STEP(F1, c, d, a, b, in[14] + 0xa679438e, 17);
MD5STEP(F1, b, c, d, a, in[15] + 0x49b40821, 22);
MD5STEP(F2, a, b, c, d, in[1] + 0xf61e2562, 5);
MD5STEP(F2, d, a, b, c, in[6] + 0xc040b340, 9);
MD5STEP(F2, c, d, a, b, in[11] + 0x265e5a51, 14);
MD5STEP(F2, b, c, d, a, in[0] + 0xe9b6c7aa, 20);
MD5STEP(F2, a, b, c, d, in[5] + 0xd62f105d, 5);
MD5STEP(F2, d, a, b, c, in[10] + 0x02441453, 9);
MD5STEP(F2, c, d, a, b, in[15] + 0xd8a1e681, 14);
MD5STEP(F2, b, c, d, a, in[4] + 0xe7d3fbc8, 20);
MD5STEP(F2, a, b, c, d, in[9] + 0x21e1cde6, 5);
MD5STEP(F2, d, a, b, c, in[14] + 0xc33707d6, 9);
MD5STEP(F2, c, d, a, b, in[3] + 0xf4d50d87, 14);
MD5STEP(F2, b, c, d, a, in[8] + 0x455a14ed, 20);
MD5STEP(F2, a, b, c, d, in[13] + 0xa9e3e905, 5);
MD5STEP(F2, d, a, b, c, in[2] + 0xfcefa3f8, 9);
MD5STEP(F2, c, d, a, b, in[7] + 0x676f02d9, 14);
MD5STEP(F2, b, c, d, a, in[12] + 0x8d2a4c8a, 20);
MD5STEP(F3, a, b, c, d, in[5] + 0xfffa3942, 4);
MD5STEP(F3, d, a, b, c, in[8] + 0x8771f681, 11);
MD5STEP(F3, c, d, a, b, in[11] + 0x6d9d6122, 16);
MD5STEP(F3, b, c, d, a, in[14] + 0xfde5380c, 23);
MD5STEP(F3, a, b, c, d, in[1] + 0xa4beea44, 4);
MD5STEP(F3, d, a, b, c, in[4] + 0x4bdecfa9, 11);
MD5STEP(F3, c, d, a, b, in[7] + 0xf6bb4b60, 16);
MD5STEP(F3, b, c, d, a, in[10] + 0xbebfbc70, 23);
MD5STEP(F3, a, b, c, d, in[13] + 0x289b7ec6, 4);
MD5STEP(F3, d, a, b, c, in[0] + 0xeaa127fa, 11);
MD5STEP(F3, c, d, a, b, in[3] + 0xd4ef3085, 16);
MD5STEP(F3, b, c, d, a, in[6] + 0x04881d05, 23);
MD5STEP(F3, a, b, c, d, in[9] + 0xd9d4d039, 4);
MD5STEP(F3, d, a, b, c, in[12] + 0xe6db99e5, 11);
MD5STEP(F3, c, d, a, b, in[15] + 0x1fa27cf8, 16);
MD5STEP(F3, b, c, d, a, in[2] + 0xc4ac5665, 23);
MD5STEP(F4, a, b, c, d, in[0] + 0xf4292244, 6);
MD5STEP(F4, d, a, b, c, in[7] + 0x432aff97, 10);
MD5STEP(F4, c, d, a, b, in[14] + 0xab9423a7, 15);
MD5STEP(F4, b, c, d, a, in[5] + 0xfc93a039, 21);
MD5STEP(F4, a, b, c, d, in[12] + 0x655b59c3, 6);
MD5STEP(F4, d, a, b, c, in[3] + 0x8f0ccc92, 10);
MD5STEP(F4, c, d, a, b, in[10] + 0xffeff47d, 15);
MD5STEP(F4, b, c, d, a, in[1] + 0x85845dd1, 21);
MD5STEP(F4, a, b, c, d, in[8] + 0x6fa87e4f, 6);
MD5STEP(F4, d, a, b, c, in[15] + 0xfe2ce6e0, 10);
MD5STEP(F4, c, d, a, b, in[6] + 0xa3014314, 15);
MD5STEP(F4, b, c, d, a, in[13] + 0x4e0811a1, 21);
MD5STEP(F4, a, b, c, d, in[4] + 0xf7537e82, 6);
MD5STEP(F4, d, a, b, c, in[11] + 0xbd3af235, 10);
MD5STEP(F4, c, d, a, b, in[2] + 0x2ad7d2bb, 15);
MD5STEP(F4, b, c, d, a, in[9] + 0xeb86d391, 21);
buf[0] += a;
buf[1] += b;
buf[2] += c;
buf[3] += d;
}
MD5Digest::MD5Digest()
{
Reset();
}
void MD5Digest::Reset()
{
buf[0] = 0x67452301;
buf[1] = 0xefcdab89;
buf[2] = 0x98badcfe;
buf[3] = 0x10325476;
bits[0] = 0;
bits[1] = 0;
std::memset(in, 0, sizeof(in));
}
void MD5Digest::Update(const void* pData, u32 cbData)
{
u32 t;
const u8* pByteData = reinterpret_cast<const u8*>(pData);
/* Update bitcount */
t = this->bits[0];
if ((this->bits[0] = t + ((u32)cbData << 3)) < t)
this->bits[1]++; /* Carry from low to high */
this->bits[1] += cbData >> 29;
t = (t >> 3) & 0x3f; /* Bytes already in shsInfo->data */
/* Handle any leading odd-sized chunks */
if (t)
{
u8* p = (u8*)this->in + t;
t = 64 - t;
if (cbData < t)
{
std::memcpy(p, pByteData, cbData);
return;
}
std::memcpy(p, pByteData, t);
MD5Transform(this->buf, (u32*)this->in);
pByteData += t;
cbData -= t;
}
/* Process data in 64-byte chunks */
while (cbData >= 64)
{
std::memcpy(this->in, pByteData, 64);
MD5Transform(this->buf, (u32*)this->in);
pByteData += 64;
cbData -= 64;
}
/* Handle any remaining bytes of data. */
std::memcpy(this->in, pByteData, cbData);
}
void MD5Digest::Final(u8 Digest[16])
{
u32 count;
u8* p;
/* Compute number of bytes mod 64 */
count = (this->bits[0] >> 3) & 0x3F;
/* Set the first char of padding to 0x80. This is safe since there is
always at least one byte free */
p = this->in + count;
*p++ = 0x80;
/* Bytes of padding needed to make 64 bytes */
count = 64 - 1 - count;
/* Pad out to 56 mod 64 */
if (count < 8)
{
/* Two lots of padding: Pad the first block to 64 bytes */
std::memset(p, 0, count);
MD5Transform(this->buf, (u32*)this->in);
/* Now fill the next block with 56 bytes */
std::memset(this->in, 0, 56);
}
else
{
/* Pad block to 56 bytes */
std::memset(p, 0, count - 8);
}
/* Append length in bits and transform */
((u32*)this->in)[14] = this->bits[0];
((u32*)this->in)[15] = this->bits[1];
MD5Transform(this->buf, (u32*)this->in);
std::memcpy(Digest, this->buf, 16);
}

20
common/MD5Digest.h Normal file
View File

@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Types.h"
class MD5Digest
{
public:
MD5Digest();
void Update(const void* pData, u32 cbData);
void Final(u8 Digest[16]);
void Reset();
private:
u32 buf[4];
u32 bits[2];
u8 in[64];
};

85
common/MRCHelpers.h Normal file
View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#ifndef __OBJC__
#error This header is for use with Objective-C++ only.
#endif
#if __has_feature(objc_arc)
#error This file is for manual reference counting! Compile without -fobjc-arc
#endif
#pragma once
#include <cstddef>
#include <utility>
/// Managed Obj-C pointer
template <typename T>
class MRCOwned
{
T ptr;
MRCOwned(T ptr): ptr(ptr) {}
public:
MRCOwned(): ptr(nullptr) {}
MRCOwned(std::nullptr_t): ptr(nullptr) {}
MRCOwned(MRCOwned&& other)
: ptr(other.ptr)
{
other.ptr = nullptr;
}
MRCOwned(const MRCOwned& other)
: ptr(other.ptr)
{
[ptr retain];
}
~MRCOwned()
{
if (ptr)
[ptr release];
}
operator T() const { return ptr; }
MRCOwned& operator=(const MRCOwned& other)
{
[other.ptr retain];
if (ptr)
[ptr release];
ptr = other.ptr;
return *this;
}
MRCOwned& operator=(MRCOwned&& other)
{
std::swap(ptr, other.ptr);
return *this;
}
void Reset()
{
[ptr release];
ptr = nullptr;
}
T Get() const { return ptr; }
static MRCOwned Transfer(T ptr)
{
return MRCOwned(ptr);
}
static MRCOwned Retain(T ptr)
{
[ptr retain];
return MRCOwned(ptr);
}
};
/// Take ownership of an Obj-C pointer (equivalent to __bridge_transfer)
template<typename T>
static inline MRCOwned<T> MRCTransfer(T ptr)
{
return MRCOwned<T>::Transfer(ptr);
}
/// Retain an Obj-C pointer (equivalent to __bridge)
template<typename T>
static inline MRCOwned<T> MRCRetain(T ptr)
{
return MRCOwned<T>::Retain(ptr);
}

View File

@@ -0,0 +1,342 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "MemorySettingsInterface.h"
#include "Error.h"
#include "StringUtil.h"
MemorySettingsInterface::MemorySettingsInterface() = default;
MemorySettingsInterface::~MemorySettingsInterface() = default;
bool MemorySettingsInterface::Save(Error* error)
{
Error::SetStringView(error, "Memory settings cannot be saved.");
return false;
}
void MemorySettingsInterface::Clear()
{
m_sections.clear();
}
bool MemorySettingsInterface::IsEmpty()
{
return m_sections.empty();
}
bool MemorySettingsInterface::GetIntValue(const char* section, const char* key, s32* value) const
{
const auto sit = m_sections.find(section);
if (sit == m_sections.end())
return false;
const auto iter = sit->second.find(key);
if (iter == sit->second.end())
return false;
std::optional<s32> parsed = StringUtil::FromChars<s32>(iter->second, 10);
if (!parsed.has_value())
return false;
*value = parsed.value();
return true;
}
bool MemorySettingsInterface::GetUIntValue(const char* section, const char* key, u32* value) const
{
const auto sit = m_sections.find(section);
if (sit == m_sections.end())
return false;
const auto iter = sit->second.find(key);
if (iter == sit->second.end())
return false;
std::optional<u32> parsed = StringUtil::FromChars<u32>(iter->second, 10);
if (!parsed.has_value())
return false;
*value = parsed.value();
return true;
}
bool MemorySettingsInterface::GetFloatValue(const char* section, const char* key, float* value) const
{
const auto sit = m_sections.find(section);
if (sit == m_sections.end())
return false;
const auto iter = sit->second.find(key);
if (iter == sit->second.end())
return false;
std::optional<float> parsed = StringUtil::FromChars<float>(iter->second);
if (!parsed.has_value())
return false;
*value = parsed.value();
return true;
}
bool MemorySettingsInterface::GetDoubleValue(const char* section, const char* key, double* value) const
{
const auto sit = m_sections.find(section);
if (sit == m_sections.end())
return false;
const auto iter = sit->second.find(key);
if (iter == sit->second.end())
return false;
std::optional<double> parsed = StringUtil::FromChars<double>(iter->second);
if (!parsed.has_value())
return false;
*value = parsed.value();
return true;
}
bool MemorySettingsInterface::GetBoolValue(const char* section, const char* key, bool* value) const
{
const auto sit = m_sections.find(section);
if (sit == m_sections.end())
return false;
const auto iter = sit->second.find(key);
if (iter == sit->second.end())
return false;
std::optional<bool> parsed = StringUtil::FromChars<bool>(iter->second);
if (!parsed.has_value())
return false;
*value = parsed.value();
return true;
}
bool MemorySettingsInterface::GetStringValue(const char* section, const char* key, std::string* value) const
{
const auto sit = m_sections.find(section);
if (sit == m_sections.end())
return false;
const auto iter = sit->second.find(key);
if (iter == sit->second.end())
return false;
*value = iter->second;
return true;
}
bool MemorySettingsInterface::GetStringValue(const char* section, const char* key, SmallStringBase* value) const
{
const auto sit = m_sections.find(section);
if (sit == m_sections.end())
return false;
const auto iter = sit->second.find(key);
if (iter == sit->second.end())
return false;
value->assign(iter->second);
return true;
}
void MemorySettingsInterface::SetIntValue(const char* section, const char* key, s32 value)
{
SetValue(section, key, std::to_string(value));
}
void MemorySettingsInterface::SetUIntValue(const char* section, const char* key, u32 value)
{
SetValue(section, key, std::to_string(value));
}
void MemorySettingsInterface::SetFloatValue(const char* section, const char* key, float value)
{
SetValue(section, key, std::to_string(value));
}
void MemorySettingsInterface::SetDoubleValue(const char* section, const char* key, double value)
{
SetValue(section, key, std::to_string(value));
}
void MemorySettingsInterface::SetBoolValue(const char* section, const char* key, bool value)
{
SetValue(section, key, std::to_string(value));
}
void MemorySettingsInterface::SetStringValue(const char* section, const char* key, const char* value)
{
SetValue(section, key, value);
}
std::vector<std::pair<std::string, std::string>> MemorySettingsInterface::GetKeyValueList(const char* section) const
{
std::vector<std::pair<std::string, std::string>> output;
auto sit = m_sections.find(section);
if (sit != m_sections.end())
{
for (const auto& it : sit->second)
output.emplace_back(it.first, it.second);
}
return output;
}
void MemorySettingsInterface::SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items)
{
auto sit = m_sections.find(section);
sit->second.clear();
for (const auto& [key, value] : items)
sit->second.emplace(key, value);
}
void MemorySettingsInterface::SetValue(const char* section, const char* key, std::string value)
{
auto sit = m_sections.find(section);
if (sit == m_sections.end())
sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first;
const auto range = sit->second.equal_range(key);
if (range.first == sit->second.end())
{
sit->second.emplace(std::string(key), std::move(value));
return;
}
auto iter = range.first;
iter->second = std::move(value);
++iter;
// remove other values
while (iter != range.second)
{
auto remove = iter++;
sit->second.erase(remove);
}
}
std::vector<std::string> MemorySettingsInterface::GetStringList(const char* section, const char* key) const
{
std::vector<std::string> ret;
const auto sit = m_sections.find(section);
if (sit != m_sections.end())
{
const auto range = sit->second.equal_range(key);
for (auto iter = range.first; iter != range.second; ++iter)
ret.emplace_back(iter->second);
}
return ret;
}
void MemorySettingsInterface::SetStringList(const char* section, const char* key, const std::vector<std::string>& items)
{
auto sit = m_sections.find(section);
if (sit == m_sections.end())
sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first;
const auto range = sit->second.equal_range(key);
for (auto iter = range.first; iter != range.second;)
sit->second.erase(iter++);
std::string_view keysv(key);
for (const std::string& value : items)
sit->second.emplace(keysv, value);
}
bool MemorySettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item)
{
auto sit = m_sections.find(section);
if (sit == m_sections.end())
sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first;
const auto range = sit->second.equal_range(key);
bool result = false;
for (auto iter = range.first; iter != range.second;)
{
if (iter->second == item)
{
sit->second.erase(iter++);
result = true;
}
else
{
++iter;
}
}
return result;
}
bool MemorySettingsInterface::AddToStringList(const char* section, const char* key, const char* item)
{
auto sit = m_sections.find(section);
if (sit == m_sections.end())
sit = m_sections.emplace(std::make_pair(std::string(section), KeyMap())).first;
const auto range = sit->second.equal_range(key);
for (auto iter = range.first; iter != range.second; ++iter)
{
if (iter->second == item)
return false;
}
sit->second.emplace(std::string(key), std::string(item));
return true;
}
bool MemorySettingsInterface::ContainsValue(const char* section, const char* key) const
{
const auto sit = m_sections.find(section);
if (sit == m_sections.end())
return false;
return (sit->second.find(key) != sit->second.end());
}
void MemorySettingsInterface::DeleteValue(const char* section, const char* key)
{
auto sit = m_sections.find(section);
if (sit == m_sections.end())
return;
const auto range = sit->second.equal_range(key);
for (auto iter = range.first; iter != range.second;)
sit->second.erase(iter++);
}
void MemorySettingsInterface::ClearSection(const char* section)
{
auto sit = m_sections.find(section);
if (sit == m_sections.end())
return;
m_sections.erase(sit);
}
void MemorySettingsInterface::RemoveSection(const char* section)
{
auto sit = m_sections.find(section);
if (sit == m_sections.end())
return;
m_sections.erase(sit);
}
void MemorySettingsInterface::RemoveEmptySections()
{
for (auto sit = m_sections.begin(); sit != m_sections.end();)
{
if (sit->second.size() > 0)
{
++sit;
continue;
}
sit = m_sections.erase(sit);
}
}

View File

@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "HeterogeneousContainers.h"
#include "SettingsInterface.h"
#include <string>
class MemorySettingsInterface final : public SettingsInterface
{
public:
MemorySettingsInterface();
~MemorySettingsInterface();
bool Save(Error* error = nullptr) override;
void Clear() override;
bool IsEmpty() override;
bool GetIntValue(const char* section, const char* key, s32* value) const override;
bool GetUIntValue(const char* section, const char* key, u32* value) const override;
bool GetFloatValue(const char* section, const char* key, float* value) const override;
bool GetDoubleValue(const char* section, const char* key, double* value) const override;
bool GetBoolValue(const char* section, const char* key, bool* value) const override;
bool GetStringValue(const char* section, const char* key, std::string* value) const override;
bool GetStringValue(const char* section, const char* key, SmallStringBase* value) const override;
void SetIntValue(const char* section, const char* key, s32 value) override;
void SetUIntValue(const char* section, const char* key, u32 value) override;
void SetFloatValue(const char* section, const char* key, float value) override;
void SetDoubleValue(const char* section, const char* key, double value) override;
void SetBoolValue(const char* section, const char* key, bool value) override;
void SetStringValue(const char* section, const char* key, const char* value) override;
std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const override;
void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) override;
bool ContainsValue(const char* section, const char* key) const override;
void DeleteValue(const char* section, const char* key) override;
void ClearSection(const char* section) override;
void RemoveSection(const char* section) override;
void RemoveEmptySections() override;
std::vector<std::string> GetStringList(const char* section, const char* key) const override;
void SetStringList(const char* section, const char* key, const std::vector<std::string>& items) override;
bool RemoveFromStringList(const char* section, const char* key, const char* item) override;
bool AddToStringList(const char* section, const char* key, const char* item) override;
// default parameter overloads
using SettingsInterface::GetBoolValue;
using SettingsInterface::GetDoubleValue;
using SettingsInterface::GetFloatValue;
using SettingsInterface::GetIntValue;
using SettingsInterface::GetStringValue;
using SettingsInterface::GetUIntValue;
private:
using KeyMap = UnorderedStringMultimap<std::string>;
using SectionMap = UnorderedStringMap<KeyMap>;
void SetValue(const char* section, const char* key, std::string value);
SectionMap m_sections;
};

87
common/Path.h Normal file
View File

@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Defs.h"
#include <string>
#include <string_view>
#include <vector>
namespace Path
{
/// Converts any forward slashes to backslashes on Win32.
std::string ToNativePath(const std::string_view path);
void ToNativePath(std::string* path);
/// Builds a path relative to the specified file
std::string BuildRelativePath(const std::string_view filename, const std::string_view new_filename);
/// Joins path components together, producing a new path.
std::string Combine(const std::string_view base, const std::string_view next);
/// Removes all .. and . components from a path.
std::string Canonicalize(const std::string_view path);
void Canonicalize(std::string* path);
/// Sanitizes a filename for use in a filesystem.
std::string SanitizeFileName(const std::string_view str, bool strip_slashes = true);
void SanitizeFileName(std::string* str, bool strip_slashes = true);
/// Returns true if the specified filename is valid on this operating system.
bool IsValidFileName(const std::string_view str, bool allow_slashes = false);
/// Returns true if the specified path is an absolute path (C:\Path on Windows or /path on Unix).
bool IsAbsolute(const std::string_view path);
/// Resolves any symbolic links in the specified path.
std::string RealPath(const std::string_view path);
/// Makes the specified path relative to another (e.g. /a/b/c, /a/b -> ../c).
/// Both paths must be relative, otherwise this function will just return the input path.
std::string MakeRelative(const std::string_view path, const std::string_view relative_to);
/// Returns a view of the extension of a filename.
std::string_view GetExtension(const std::string_view path);
/// Removes the extension of a filename.
std::string_view StripExtension(const std::string_view path);
/// Replaces the extension of a filename with another.
std::string ReplaceExtension(const std::string_view path, const std::string_view new_extension);
/// Returns the directory component of a filename.
std::string_view GetDirectory(const std::string_view path);
/// Returns the filename component of a filename.
std::string_view GetFileName(const std::string_view path);
/// Returns the file title (less the extension and path) from a filename.
std::string_view GetFileTitle(const std::string_view path);
/// Changes the filename in a path.
std::string ChangeFileName(const std::string_view path, const std::string_view new_filename);
void ChangeFileName(std::string* path, const std::string_view new_filename);
/// Appends a directory to a path.
std::string AppendDirectory(const std::string_view path, const std::string_view new_dir);
void AppendDirectory(std::string* path, const std::string_view new_dir);
/// Splits a path into its components, handling both Windows and Unix separators.
std::vector<std::string_view> SplitWindowsPath(const std::string_view path);
std::string JoinWindowsPath(const std::vector<std::string_view>& components);
/// Splits a path into its components, only handling native separators.
std::vector<std::string_view> SplitNativePath(const std::string_view path);
std::string JoinNativePath(const std::vector<std::string_view>& components);
/// URL encodes the specified string.
std::string URLEncode(std::string_view str);
/// Decodes the specified escaped string.
std::string URLDecode(std::string_view str);
/// Returns a URL for a given path. The path should be absolute.
std::string CreateFileURL(std::string_view path);
} // namespace Path

165
common/Pcsx2Defs.h Normal file
View File

@@ -0,0 +1,165 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Types.h"
#include <bit>
#include <cstddef>
// --------------------------------------------------------------------------------------
// Dev / Debug conditionals - Consts for using if() statements instead of uglier #ifdef.
// --------------------------------------------------------------------------------------
#ifdef PCSX2_DEVBUILD
static constexpr bool IsDevBuild = true;
#else
static constexpr bool IsDevBuild = false;
#endif
#ifdef PCSX2_DEBUG
static constexpr bool IsDebugBuild = true;
#else
static constexpr bool IsDebugBuild = false;
#endif
// Defines the memory page size for the target platform at compilation.
#if defined(OVERRIDE_HOST_PAGE_SIZE)
static constexpr unsigned int __pagesize = OVERRIDE_HOST_PAGE_SIZE;
static constexpr unsigned int __pagemask = __pagesize - 1;
static constexpr unsigned int __pageshift = std::bit_width(__pagemask);
#elif defined(_M_ARM64)
// Apple Silicon uses 16KB pages and 128 byte cache lines.
static constexpr unsigned int __pagesize = 0x4000;
static constexpr unsigned int __pageshift = 14;
static constexpr unsigned int __pagemask = __pagesize - 1;
#else
// X86 uses a 4KB granularity and 64 byte cache lines.
static constexpr unsigned int __pagesize = 0x1000;
static constexpr unsigned int __pageshift = 12;
static constexpr unsigned int __pagemask = __pagesize - 1;
#endif
#if defined(OVERRIDE_HOST_CACHE_LINE_SIZE)
static constexpr unsigned int __cachelinesize = OVERRIDE_HOST_CACHE_LINE_SIZE;
#elif defined(_M_ARM64)
static constexpr unsigned int __cachelinesize = 128;
#else
static constexpr unsigned int __cachelinesize = 64;
#endif
// We use 4KB alignment for globals for both Apple and x86 platforms, since computing the
// address on ARM64 is a single instruction (adrp).
static constexpr unsigned int __pagealignsize = 0x1000;
// --------------------------------------------------------------------------------------
// Microsoft Visual Studio
// --------------------------------------------------------------------------------------
#ifdef _MSC_VER
#define __forceinline_odr __forceinline
#define __noinline __declspec(noinline)
#define __noreturn __declspec(noreturn)
#define RESTRICT __restrict
#define ASSUME(x) __assume(x)
#else
// --------------------------------------------------------------------------------------
// GCC / Clang Compilers Section
// --------------------------------------------------------------------------------------
// SysV ABI passes vector parameters through registers unconditionally.
#ifndef _WIN32
#define __vectorcall
#endif
// Inlining note: GCC needs ((unused)) attributes defined on inlined functions to suppress
// warnings when a static inlined function isn't used in the scope of a single file (which
// happens *by design* like all the friggen time >_<)
// __forceinline_odr is for member functions that are defined in headers. MSVC can't specify
// inline and __forceinline at the same time, but it required to not get ODR errors in GCC.
#define __forceinline __attribute__((always_inline, unused))
#define __forceinline_odr __forceinline inline
#define __noinline __attribute__((noinline))
#define __noreturn __attribute__((noreturn))
#define RESTRICT __restrict__
#define ASSUME(x) \
do \
{ \
if (!(x)) \
__builtin_unreachable(); \
} while (0)
#endif
// --------------------------------------------------------------------------------------
// __releaseinline / __ri -- a forceinline macro that is enabled for RELEASE/PUBLIC builds ONLY.
// --------------------------------------------------------------------------------------
// This is useful because forceinline can make certain types of debugging problematic since
// functions that look like they should be called won't breakpoint since their code is
// inlined, and it can make stack traces confusing or near useless.
//
// Use __releaseinline for things which are generally large functions where trace debugging
// from Devel builds is likely useful; but which should be inlined in an optimized Release
// environment.
//
#define __fi __forceinline
#ifdef PCSX2_DEVBUILD
#define __ri
#else
#define __ri __fi
#endif
//////////////////////////////////////////////////////////////////////////////////////////
// Safe deallocation macros -- checks pointer validity (non-null) when needed, and sets
// pointer to null after deallocation.
#define safe_delete(ptr) (delete (ptr), (ptr) = nullptr)
#define safe_delete_array(ptr) (delete[] (ptr), (ptr) = nullptr)
#define safe_free(ptr) (std::free(ptr), (ptr) = nullptr)
// --------------------------------------------------------------------------------------
// DeclareNoncopyableObject
// --------------------------------------------------------------------------------------
// This macro provides an easy and clean method for ensuring objects are not copyable.
// Simply add the macro to the head or tail of your class declaration, and attempts to
// copy the class will give you a moderately obtuse compiler error.
//
#ifndef DeclareNoncopyableObject
#define DeclareNoncopyableObject(classname) \
public: \
classname(const classname&) = delete; \
classname& operator=(const classname&) = delete
#endif
// --------------------------------------------------------------------------------------
// Handy Human-readable constants for common immediate values (_16kb -> _4gb)
static constexpr sptr _1kb = 1024 * 1;
static constexpr sptr _4kb = _1kb * 4;
static constexpr sptr _16kb = _1kb * 16;
static constexpr sptr _32kb = _1kb * 32;
static constexpr sptr _64kb = _1kb * 64;
static constexpr sptr _128kb = _1kb * 128;
static constexpr sptr _256kb = _1kb * 256;
static constexpr s64 _1mb = 1024 * 1024;
static constexpr s64 _8mb = _1mb * 8;
static constexpr s64 _16mb = _1mb * 16;
static constexpr s64 _32mb = _1mb * 32;
static constexpr s64 _64mb = _1mb * 64;
static constexpr s64 _256mb = _1mb * 256;
static constexpr s64 _1gb = _1mb * 1024;
static constexpr s64 _4gb = _1gb * 4;
// Disable some spammy warnings which wx appeared to disable.
// We probably should fix these at some point.
#ifdef _MSC_VER
#pragma warning(disable : 4244) // warning C4244: 'initializing': conversion from 'uptr' to 'uint', possible loss of data
#pragma warning(disable : 4267) // warning C4267: 'initializing': conversion from 'size_t' to 'uint', possible loss of data
#endif

114
common/Pcsx2Types.h Normal file
View File

@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <cstdint>
// --------------------------------------------------------------------------------------
// Basic Atomic Types
// --------------------------------------------------------------------------------------
using s8 = int8_t;
using s16 = int16_t;
using s32 = int32_t;
using s64 = int64_t;
using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;
using u64 = uint64_t;
using uptr = uintptr_t;
using sptr = intptr_t;
using uint = unsigned int;
// --------------------------------------------------------------------------------------
// u128 / s128 - A rough-and-ready cross platform 128-bit datatype, Non-SSE style.
// --------------------------------------------------------------------------------------
// Note: These structs don't provide any additional constructors because C++ doesn't allow
// the use of datatypes with constructors in unions (and since unions aren't the primary
// uses of these types, that means we can't have constructors). Embedded functions for
// performing explicit conversion from 64 and 32 bit values are provided instead.
//
union u128
{
struct
{
u64 lo;
u64 hi;
};
u64 _u64[2];
u32 _u32[4];
u16 _u16[8];
u8 _u8[16];
// Explicit conversion from u64. Zero-extends the source through 128 bits.
static u128 From64(u64 src)
{
u128 retval;
retval.lo = src;
retval.hi = 0;
return retval;
}
// Explicit conversion from u32. Zero-extends the source through 128 bits.
static u128 From32(u32 src)
{
u128 retval;
retval._u32[0] = src;
retval._u32[1] = 0;
retval.hi = 0;
return retval;
}
operator u32() const { return _u32[0]; }
operator u16() const { return _u16[0]; }
operator u8() const { return _u8[0]; }
bool operator==(const u128& right) const
{
return (lo == right.lo) && (hi == right.hi);
}
bool operator!=(const u128& right) const
{
return (lo != right.lo) || (hi != right.hi);
}
};
struct s128
{
s64 lo;
s64 hi;
// explicit conversion from s64, with sign extension.
static s128 From64(s64 src)
{
s128 retval = {src, (src < 0) ? -1 : 0};
return retval;
}
// explicit conversion from s32, with sign extension.
static s128 From64(s32 src)
{
s128 retval = {src, (src < 0) ? -1 : 0};
return retval;
}
operator u32() const { return (s32)lo; }
operator u16() const { return (s16)lo; }
operator u8() const { return (s8)lo; }
bool operator==(const s128& right) const
{
return (lo == right.lo) && (hi == right.hi);
}
bool operator!=(const s128& right) const
{
return (lo != right.lo) || (hi != right.hi);
}
};

215
common/Perf.cpp Normal file
View File

@@ -0,0 +1,215 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/Perf.h"
#include "common/Pcsx2Defs.h"
#include "common/Assertions.h"
#include "common/StringUtil.h"
#ifdef ENABLE_VTUNE
#include "jitprofiling.h"
#endif
#include <array>
#include <cstring>
#ifdef __linux__
#include <atomic>
#include <ctime>
#include <mutex>
#include <elf.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#endif
//#define ProfileWithPerf
//#define ProfileWithPerfJitDump
#if defined(ENABLE_VTUNE) && defined(_WIN32)
#pragma comment(lib, "jitprofiling.lib")
#endif
namespace Perf
{
Group any("");
Group ee("EE");
Group iop("IOP");
Group vu0("VU0");
Group vu1("VU1");
Group vif("VIF");
// Perf is only supported on linux
#if defined(__linux__) && defined(ProfileWithPerf)
static std::FILE* s_map_file = nullptr;
static bool s_map_file_opened = false;
static std::mutex s_mutex;
static void RegisterMethod(const void* ptr, size_t size, const char* symbol)
{
std::unique_lock lock(s_mutex);
if (!s_map_file)
{
if (s_map_file_opened)
return;
char file[256];
snprintf(file, std::size(file), "/tmp/perf-%d.map", getpid());
s_map_file = std::fopen(file, "wb");
s_map_file_opened = true;
if (!s_map_file)
return;
}
std::fprintf(s_map_file, "%" PRIx64 " %zx %s\n", static_cast<u64>(reinterpret_cast<uintptr_t>(ptr)), size, symbol);
std::fflush(s_map_file);
}
#elif defined(__linux__) && defined(ProfileWithPerfJitDump)
enum : u32
{
JIT_CODE_LOAD = 0,
JIT_CODE_MOVE = 1,
JIT_CODE_DEBUG_INFO = 2,
JIT_CODE_CLOSE = 3,
JIT_CODE_UNWINDING_INFO = 4
};
#pragma pack(push, 1)
struct JITDUMP_HEADER
{
u32 magic = 0x4A695444; // JiTD
u32 version = 1;
u32 header_size = sizeof(JITDUMP_HEADER);
u32 elf_mach;
u32 pad1 = 0;
u32 pid;
u64 timestamp;
u64 flags = 0;
};
struct JITDUMP_RECORD_HEADER
{
u32 id;
u32 total_size;
u64 timestamp;
};
struct JITDUMP_CODE_LOAD
{
JITDUMP_RECORD_HEADER header;
u32 pid;
u32 tid;
u64 vma;
u64 code_addr;
u64 code_size;
u64 code_index;
// name
};
#pragma pack(pop)
static u64 JitDumpTimestamp()
{
struct timespec ts = {};
clock_gettime(CLOCK_MONOTONIC, &ts);
return (static_cast<u64>(ts.tv_sec) * 1000000000ULL) + static_cast<u64>(ts.tv_nsec);
}
static FILE* s_jitdump_file = nullptr;
static bool s_jitdump_file_opened = false;
static std::mutex s_jitdump_mutex;
static u32 s_jitdump_record_id;
static void RegisterMethod(const void* ptr, size_t size, const char* symbol)
{
const u32 namelen = std::strlen(symbol) + 1;
std::unique_lock lock(s_jitdump_mutex);
if (!s_jitdump_file)
{
if (!s_jitdump_file_opened)
{
char file[256];
snprintf(file, std::size(file), "jit-%d.dump", getpid());
s_jitdump_file = fopen(file, "w+b");
s_jitdump_file_opened = true;
if (!s_jitdump_file)
return;
}
void* perf_marker = mmap(nullptr, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE, fileno(s_jitdump_file), 0);
pxAssertRel(perf_marker != MAP_FAILED, "Map perf marker");
JITDUMP_HEADER jh = {};
#if defined(_M_X86)
jh.elf_mach = EM_X86_64;
#elif defined(_M_ARM64)
jh.elf_mach = EM_AARCH64;
#else
#error Unhandled architecture.
#endif
jh.pid = getpid();
jh.timestamp = JitDumpTimestamp();
std::fwrite(&jh, sizeof(jh), 1, s_jitdump_file);
}
JITDUMP_CODE_LOAD cl = {};
cl.header.id = JIT_CODE_LOAD;
cl.header.total_size = sizeof(cl) + namelen + static_cast<u32>(size);
cl.header.timestamp = JitDumpTimestamp();
cl.pid = getpid();
cl.tid = syscall(SYS_gettid);
cl.vma = 0;
cl.code_addr = static_cast<u64>(reinterpret_cast<uintptr_t>(ptr));
cl.code_size = static_cast<u64>(size);
cl.code_index = s_jitdump_record_id++;
std::fwrite(&cl, sizeof(cl), 1, s_jitdump_file);
std::fwrite(symbol, namelen, 1, s_jitdump_file);
std::fwrite(ptr, size, 1, s_jitdump_file);
std::fflush(s_jitdump_file);
}
#elif defined(ENABLE_VTUNE)
static void RegisterMethod(const void* ptr, size_t size, const char* symbol)
{
iJIT_Method_Load_V2 ml = {};
ml.method_id = iJIT_GetNewMethodID();
ml.method_name = const_cast<char*>(symbol);
ml.method_load_address = const_cast<void*>(ptr);
ml.method_size = static_cast<unsigned int>(size);
iJIT_NotifyEvent(iJVM_EVENT_TYPE_METHOD_LOAD_FINISHED_V2, &ml);
}
#endif
#if (defined(__linux__) && (defined(ProfileWithPerf) || defined(ProfileWithPerfJitDump))) || defined(ENABLE_VTUNE)
void Group::Register(const void* ptr, size_t size, const char* symbol)
{
char full_symbol[128];
if (HasPrefix())
std::snprintf(full_symbol, std::size(full_symbol), "%s_%s", m_prefix, symbol);
else
StringUtil::Strlcpy(full_symbol, symbol, std::size(full_symbol));
RegisterMethod(ptr, size, full_symbol);
}
void Group::RegisterPC(const void* ptr, size_t size, u32 pc)
{
char full_symbol[128];
if (HasPrefix())
std::snprintf(full_symbol, std::size(full_symbol), "%s_%08X", m_prefix, pc);
else
std::snprintf(full_symbol, std::size(full_symbol), "%08X", pc);
RegisterMethod(ptr, size, full_symbol);
}
void Group::RegisterKey(const void* ptr, size_t size, const char* prefix, u64 key)
{
char full_symbol[128];
if (HasPrefix())
std::snprintf(full_symbol, std::size(full_symbol), "%s_%s%016" PRIX64, m_prefix, prefix, key);
else
std::snprintf(full_symbol, std::size(full_symbol), "%s%016" PRIX64, prefix, key);
RegisterMethod(ptr, size, full_symbol);
}
#else
void Group::Register(const void* ptr, size_t size, const char* symbol) {}
void Group::RegisterPC(const void* ptr, size_t size, u32 pc) {}
void Group::RegisterKey(const void* ptr, size_t size, const char* prefix, u64 key) {}
#endif
} // namespace Perf

31
common/Perf.h Normal file
View File

@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <vector>
#include <cstdio>
#include "common/Pcsx2Types.h"
namespace Perf
{
class Group
{
const char* m_prefix;
public:
constexpr Group(const char* prefix) : m_prefix(prefix) {}
bool HasPrefix() const { return (m_prefix && m_prefix[0]); }
void Register(const void* ptr, size_t size, const char* symbol);
void RegisterPC(const void* ptr, size_t size, u32 pc);
void RegisterKey(const void* ptr, size_t size, const char* prefix, u64 key);
};
extern Group any;
extern Group ee;
extern Group iop;
extern Group vu0;
extern Group vu1;
extern Group vif;
} // namespace Perf

View File

@@ -0,0 +1,4 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "PrecompiledHeader.h"

View File

@@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <memory>
#include <atomic>
#include <csignal>
#include <cerrno>
#include <cstdio>

245
common/ProgressCallback.cpp Normal file
View File

@@ -0,0 +1,245 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "PrecompiledHeader.h"
#include "Assertions.h"
#include "ProgressCallback.h"
#include "StringUtil.h"
#include "Console.h"
#include <cmath>
#include <cstdio>
#include <limits>
ProgressCallback::~ProgressCallback() {}
void ProgressCallback::SetFormattedStatusText(const char* Format, ...)
{
va_list ap;
va_start(ap, Format);
const std::string str(StringUtil::StdStringFromFormatV(Format, ap));
va_end(ap);
SetStatusText(str.c_str());
}
void ProgressCallback::DisplayFormattedError(const char* format, ...)
{
va_list ap;
va_start(ap, format);
const std::string str(StringUtil::StdStringFromFormatV(format, ap));
va_end(ap);
DisplayError(str.c_str());
}
void ProgressCallback::DisplayFormattedWarning(const char* format, ...)
{
va_list ap;
va_start(ap, format);
const std::string str(StringUtil::StdStringFromFormatV(format, ap));
va_end(ap);
DisplayWarning(str.c_str());
}
void ProgressCallback::DisplayFormattedInformation(const char* format, ...)
{
va_list ap;
va_start(ap, format);
const std::string str(StringUtil::StdStringFromFormatV(format, ap));
va_end(ap);
DisplayInformation(str.c_str());
}
void ProgressCallback::DisplayFormattedDebugMessage(const char* format, ...)
{
va_list ap;
va_start(ap, format);
const std::string str(StringUtil::StdStringFromFormatV(format, ap));
va_end(ap);
DisplayDebugMessage(str.c_str());
}
void ProgressCallback::DisplayFormattedModalError(const char* format, ...)
{
va_list ap;
va_start(ap, format);
const std::string str(StringUtil::StdStringFromFormatV(format, ap));
va_end(ap);
ModalError(str.c_str());
}
bool ProgressCallback::DisplayFormattedModalConfirmation(const char* format, ...)
{
va_list ap;
va_start(ap, format);
const std::string str(StringUtil::StdStringFromFormatV(format, ap));
va_end(ap);
return ModalConfirmation(str.c_str());
}
void ProgressCallback::DisplayFormattedModalInformation(const char* format, ...)
{
va_list ap;
va_start(ap, format);
const std::string str(StringUtil::StdStringFromFormatV(format, ap));
va_end(ap);
ModalInformation(str.c_str());
}
namespace
{
class NullProgressCallbacks final : public ProgressCallback
{
public:
void PushState() override {}
void PopState() override {}
bool IsCancelled() const override { return false; }
bool IsCancellable() const override { return false; }
void SetCancellable(bool cancellable) override {}
void SetTitle(const char* title) override {}
void SetStatusText(const char* statusText) override {}
void SetProgressRange(u32 range) override {}
void SetProgressValue(u32 value) override {}
void IncrementProgressValue() override {}
void SetProgressState(ProgressState state) override {}
void DisplayError(const char* message) override { Console.Error("%s", message); }
void DisplayWarning(const char* message) override { Console.Warning("%s", message); }
void DisplayInformation(const char* message) override { Console.WriteLn("%s", message); }
void DisplayDebugMessage(const char* message) override { DevCon.WriteLn("%s", message); }
void ModalError(const char* message) override { Console.Error(message); }
bool ModalConfirmation(const char* message) override
{
Console.WriteLn("%s", message);
return false;
}
void ModalInformation(const char* message) override { Console.WriteLn("%s", message); }
};
} // namespace
static NullProgressCallbacks s_nullProgressCallbacks;
ProgressCallback* ProgressCallback::NullProgressCallback = &s_nullProgressCallbacks;
std::unique_ptr<ProgressCallback> ProgressCallback::CreateNullProgressCallback()
{
return std::make_unique<NullProgressCallbacks>();
}
BaseProgressCallback::BaseProgressCallback()
{
}
BaseProgressCallback::~BaseProgressCallback()
{
State* pNextState = m_saved_state;
while (pNextState != NULL)
{
State* pCurrentState = pNextState;
pNextState = pCurrentState->next_saved_state;
delete pCurrentState;
}
}
void BaseProgressCallback::PushState()
{
State* pNewState = new State;
pNewState->cancellable = m_cancellable;
pNewState->status_text = m_status_text;
pNewState->progress_range = m_progress_range;
pNewState->progress_value = m_progress_value;
pNewState->base_progress_value = m_base_progress_value;
pNewState->next_saved_state = m_saved_state;
m_saved_state = pNewState;
}
void BaseProgressCallback::PopState()
{
pxAssert(m_saved_state);
State* state = m_saved_state;
m_saved_state = nullptr;
// impose the current position into the previous range
const u32 new_progress_value =
(m_progress_range != 0) ?
static_cast<u32>(((float)m_progress_value / (float)m_progress_range) * (float)state->progress_range) :
state->progress_value;
m_cancellable = state->cancellable;
m_status_text = std::move(state->status_text);
m_progress_range = state->progress_range;
m_progress_value = new_progress_value;
m_base_progress_value = state->base_progress_value;
m_saved_state = state->next_saved_state;
delete state;
}
bool BaseProgressCallback::IsCancelled() const
{
return m_cancelled;
}
bool BaseProgressCallback::IsCancellable() const
{
return m_cancellable;
}
void BaseProgressCallback::SetCancellable(bool cancellable)
{
m_cancellable = cancellable;
}
void BaseProgressCallback::SetStatusText(const char* text)
{
m_status_text = text;
}
void BaseProgressCallback::SetProgressRange(u32 range)
{
if (m_saved_state)
{
// impose the previous range on this range
m_progress_range = m_saved_state->progress_range * range;
m_base_progress_value = m_progress_value = m_saved_state->progress_value * range;
}
else
{
m_progress_range = range;
m_progress_value = 0;
m_base_progress_value = 0;
}
}
void BaseProgressCallback::SetProgressValue(u32 value)
{
SetProgressState(ProgressState::Normal);
m_progress_value = m_base_progress_value + value;
}
void BaseProgressCallback::IncrementProgressValue()
{
SetProgressValue((m_progress_value - m_base_progress_value) + 1);
}
void BaseProgressCallback::SetProgressState(ProgressState state)
{
m_progress_state = state;
}

108
common/ProgressCallback.h Normal file
View File

@@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <memory>
#include <string>
/**
* Progress callbacks, abstracts a blocking operation and allows it to report progress
* without having any dependency on the UI.
*/
class ProgressCallback
{
public:
enum class ProgressState
{
Normal,
Indeterminate,
Paused,
Error
};
virtual ~ProgressCallback();
virtual void PushState() = 0;
virtual void PopState() = 0;
virtual bool IsCancelled() const = 0;
virtual bool IsCancellable() const = 0;
virtual void SetCancellable(bool cancellable) = 0;
virtual void SetTitle(const char* title) = 0;
virtual void SetStatusText(const char* text) = 0;
virtual void SetProgressRange(u32 range) = 0;
virtual void SetProgressValue(u32 value) = 0;
virtual void IncrementProgressValue() = 0;
virtual void SetProgressState(ProgressState state) = 0;
void SetFormattedStatusText(const char* Format, ...);
virtual void DisplayError(const char* message) = 0;
virtual void DisplayWarning(const char* message) = 0;
virtual void DisplayInformation(const char* message) = 0;
virtual void DisplayDebugMessage(const char* message) = 0;
virtual void ModalError(const char* message) = 0;
virtual bool ModalConfirmation(const char* message) = 0;
virtual void ModalInformation(const char* message) = 0;
void DisplayFormattedError(const char* format, ...);
void DisplayFormattedWarning(const char* format, ...);
void DisplayFormattedInformation(const char* format, ...);
void DisplayFormattedDebugMessage(const char* format, ...);
void DisplayFormattedModalError(const char* format, ...);
bool DisplayFormattedModalConfirmation(const char* format, ...);
void DisplayFormattedModalInformation(const char* format, ...);
public:
static ProgressCallback* NullProgressCallback;
static std::unique_ptr<ProgressCallback> CreateNullProgressCallback();
};
class BaseProgressCallback : public ProgressCallback
{
public:
BaseProgressCallback();
virtual ~BaseProgressCallback();
virtual void PushState() override;
virtual void PopState() override;
virtual bool IsCancelled() const override;
virtual bool IsCancellable() const override;
virtual void SetCancellable(bool cancellable) override;
virtual void SetStatusText(const char* text) override;
virtual void SetProgressRange(u32 range) override;
virtual void SetProgressValue(u32 value) override;
virtual void IncrementProgressValue() override;
virtual void SetProgressState(ProgressState state) override;
protected:
struct State
{
State* next_saved_state;
std::string status_text;
u32 progress_range;
u32 progress_value;
u32 base_progress_value;
bool cancellable;
};
bool m_cancellable = false;
bool m_cancelled = false;
std::string m_status_text;
u32 m_progress_range = 1;
u32 m_progress_value = 0;
ProgressState m_progress_state = ProgressState::Normal;
u32 m_base_progress_value = 0;
State* m_saved_state = nullptr;
};

View File

@@ -0,0 +1,218 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "ReadbackSpinManager.h"
#include <algorithm>
static bool EventIsReadback(const ReadbackSpinManager::Event& event)
{
return event.size < 0;
}
static bool EventIsDraw(const ReadbackSpinManager::Event& event)
{
return !EventIsReadback(event);
}
static bool IsCompleted(const ReadbackSpinManager::Event& event)
{
return event.begin != event.end;
}
static int Similarity(const std::vector<ReadbackSpinManager::Event>& a, std::vector<ReadbackSpinManager::Event>& b)
{
u32 a_num_readbacks = std::count_if(a.begin(), a.end(), EventIsReadback);
u32 b_num_readbacks = std::count_if(b.begin(), b.end(), EventIsReadback);
int score = 0x10 - abs(static_cast<int>(a.size() - b.size()));
if (a_num_readbacks == b_num_readbacks)
score += 0x10000;
auto a_idx = a.begin();
auto b_idx = b.begin();
while (a_idx != a.end() && b_idx != b.end())
{
if (EventIsReadback(*a_idx) && EventIsReadback(*b_idx))
{
// Same number of events between readbacks
score += 0x1000;
}
// Try to match up on readbacks
else if (EventIsReadback(*a_idx))
{
b_idx++;
continue;
}
else if (EventIsReadback(*b_idx))
{
a_idx++;
continue;
}
else if (a_idx->size == b_idx->size)
{
// Same size
score += 0x100;
}
else if (a_idx->size / 2 <= b_idx->size && b_idx->size / 2 <= a_idx->size)
{
// Similar size
score += 0x10;
}
a_idx++;
b_idx++;
continue;
}
// Both hit the end at the same time
if (a_idx == a.end() && b_idx == b.end())
score += 0x1000;
return score;
}
static u32 PrevFrameNo(u32 frame, size_t total_frames)
{
s32 prev_frame = frame - 1;
if (prev_frame < 0)
prev_frame = total_frames - 1;
return prev_frame;
}
static u32 NextFrameNo(u32 frame, size_t total_frames)
{
u32 next_frame = frame + 1;
if (next_frame >= total_frames)
next_frame = 0;
return next_frame;
}
void ReadbackSpinManager::ReadbackRequested()
{
Event ev = {};
ev.size = -1;
m_frames[m_current_frame].push_back(ev);
// Advance reference frame idx to the next readback
while (m_frames[m_reference_frame].size() > m_reference_frame_idx &&
!EventIsReadback(m_frames[m_reference_frame][m_reference_frame_idx]))
{
m_reference_frame_idx++;
}
// ...and past it
if (m_frames[m_reference_frame].size() > m_reference_frame_idx)
m_reference_frame_idx++;
}
void ReadbackSpinManager::NextFrame()
{
u32 prev_frame_0 = PrevFrameNo(m_current_frame, std::size(m_frames));
u32 prev_frame_1 = PrevFrameNo(prev_frame_0, std::size(m_frames));
int similarity_0 = Similarity(m_frames[m_current_frame], m_frames[prev_frame_0]);
int similarity_1 = Similarity(m_frames[m_current_frame], m_frames[prev_frame_1]);
if (similarity_1 > similarity_0)
m_reference_frame = prev_frame_0;
else
m_reference_frame = m_current_frame;
m_reference_frame_idx = 0;
m_current_frame = NextFrameNo(m_current_frame, std::size(m_frames));
m_frames[m_current_frame].clear();
}
ReadbackSpinManager::DrawSubmittedReturn ReadbackSpinManager::DrawSubmitted(u64 size)
{
DrawSubmittedReturn out = {};
u32 idx = m_frames[m_current_frame].size();
out.id = idx | m_current_frame << 28;
Event ev = {};
ev.size = size;
m_frames[m_current_frame].push_back(ev);
if (m_reference_frame != m_current_frame &&
m_frames[m_reference_frame].size() > m_reference_frame_idx &&
EventIsDraw(m_frames[m_reference_frame][m_reference_frame_idx]))
{
auto find_next_draw = [this](u32 frame) -> Event* {
auto next = std::find_if(m_frames[frame].begin() + m_reference_frame_idx + 1,
m_frames[frame].end(),
EventIsDraw);
bool found = next != m_frames[frame].end();
if (!found)
{
u32 next_frame = NextFrameNo(frame, std::size(m_frames));
next = std::find_if(m_frames[next_frame].begin(), m_frames[next_frame].end(), EventIsDraw);
found = next != m_frames[next_frame].end();
}
return found ? &*next : nullptr;
};
Event* cur_draw = &m_frames[m_reference_frame][m_reference_frame_idx];
Event* next_draw = find_next_draw(m_reference_frame);
const bool is_one_frame_back = m_reference_frame == PrevFrameNo(m_current_frame, std::size(m_frames));
if ((!next_draw || !IsCompleted(*cur_draw) || !IsCompleted(*next_draw)) && is_one_frame_back)
{
// Last frame's timing data hasn't arrived, try the same spot in the frame before
u32 two_back = PrevFrameNo(m_reference_frame, std::size(m_frames));
if (m_frames[two_back].size() > m_reference_frame_idx &&
EventIsDraw(m_frames[two_back][m_reference_frame_idx]))
{
cur_draw = &m_frames[two_back][m_reference_frame_idx];
next_draw = find_next_draw(two_back);
}
}
if (next_draw && IsCompleted(*cur_draw) && IsCompleted(*next_draw) && m_spins_per_unit_time != 0)
{
u64 cur_size = cur_draw->size;
bool is_similar = cur_size / 2 <= size && size / 2 <= cur_size;
if (is_similar) // Only recommend spins if we're somewhat confident in what's going on
{
s32 current_draw_time = cur_draw->end - cur_draw->begin;
s32 gap = next_draw->begin - cur_draw->end;
// Give an extra bit of space for the draw to take a bit longer (we'll go with 1/8 longer)
s32 fill = gap - (current_draw_time >> 3);
if (fill > 0)
out.recommended_spin = static_cast<u32>(static_cast<double>(fill) * m_spins_per_unit_time);
}
}
m_reference_frame_idx++;
}
if (m_spins_per_unit_time == 0)
{
// Recommend some spinning so that we can get timing data
out.recommended_spin = 128;
}
return out;
}
void ReadbackSpinManager::DrawCompleted(u32 id, u32 begin_time, u32 end_time)
{
u32 frame_id = id >> 28;
u32 frame_off = id & ((1 << 28) - 1);
if (frame_id < std::size(m_frames) && frame_off < m_frames[frame_id].size())
{
Event& ev = m_frames[frame_id][frame_off];
ev.begin = begin_time;
ev.end = end_time;
}
}
void ReadbackSpinManager::SpinCompleted(u32 cycles, u32 begin_time, u32 end_time)
{
double elapsed = static_cast<double>(end_time - begin_time);
constexpr double decay = 15.0 / 16.0;
// Obviously it'll vary from GPU to GPU, but in my testing,
// both a Radeon Pro 5600M and Intel UHD 630 spin at about 100ns/cycle
// Note: We assume spin time is some constant times the number of cycles
// Obviously as the number of cycles gets really low, a constant offset may start being noticeable
// But this is not the case as low as 512 cycles (~50µs) on the GPUs listed above
m_total_spin_cycles = m_total_spin_cycles * decay + cycles;
m_total_spin_time = m_total_spin_time * decay + elapsed;
m_spins_per_unit_time = m_total_spin_cycles / m_total_spin_time;
}

View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <vector>
/// A class for calculating optimal spin values to trick OSes into not powering down GPUs while waiting for readbacks
class ReadbackSpinManager
{
public:
struct Event
{
s64 size;
u32 begin;
u32 end;
};
private:
double m_spins_per_unit_time = 0;
double m_total_spin_time = 0;
double m_total_spin_cycles = 0;
std::vector<Event> m_frames[3];
u32 m_current_frame = 0;
u32 m_reference_frame = 0;
u32 m_reference_frame_idx = 0;
public:
struct DrawSubmittedReturn
{
u32 id;
u32 recommended_spin;
};
/// Call when a readback is requested
void ReadbackRequested();
/// Call at the end of a frame
void NextFrame();
/// Call when a command buffer is submitted to the GPU
/// `size` is used to attempt to find patterns in submissions, and can be any metric that approximates the amount of work in a submission (draw calls, command encoders, etc)
/// Returns an id to be passed to `DrawCompleted`, and the recommended number of spin cycles to perform on the GPU in order to keep it busy
DrawSubmittedReturn DrawSubmitted(u64 size);
/// Call once a draw has been finished by the GPU and you have begin/end data for it
/// `begin_time` and `end_time` can be in any unit as long as it's consistent. It's okay if they roll over, as long as it happens less than once every few frames.
void DrawCompleted(u32 id, u32 begin_time, u32 end_time);
/// Call when a spin completes to help the manager figure out how quickly your GPU spins
void SpinCompleted(u32 cycles, u32 begin_time, u32 end_time);
/// Get the calculated number of spins per unit of time
/// Note: May be zero when there's insufficient data
double SpinsPerUnitTime() const { return m_spins_per_unit_time; }
};

22
common/RedtapeWilCom.h Normal file
View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#ifdef _WIN32
#include "common/RedtapeWindows.h"
// warning : variable 's_hrErrorLast' set but not used [-Wunused-but-set-variable]
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
#endif
#include <wil/com.h>
#ifdef __clang__
#pragma clang diagnostic pop
#endif
#endif

23
common/RedtapeWindows.h Normal file
View File

@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
// We require Windows 10+.
#ifdef _WIN32_WINNT
#undef _WIN32_WINNT
#endif
#define _WIN32_WINNT 0x0A00 // Windows 10
#include <Windows.h>
#endif

52
common/ScopedGuard.h Normal file
View File

@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <optional>
#include <utility>
/// ScopedGuard provides an object which runs a function (usually a lambda) when
/// it goes out of scope. This can be useful for releasing resources or handles
/// which do not normally have C++ types to automatically release.
template <typename T>
class ScopedGuard final
{
public:
__fi ScopedGuard(T&& func)
: m_func(std::forward<T>(func))
{
}
__fi ScopedGuard(ScopedGuard&& other)
: m_func(std::move(other.m_func))
{
other.m_func = nullptr;
}
__fi ~ScopedGuard()
{
Run();
}
ScopedGuard(const ScopedGuard&) = delete;
void operator=(const ScopedGuard&) = delete;
/// Runs the destructor function now instead of when we go out of scope.
__fi void Run()
{
if (!m_func.has_value())
return;
m_func.value()();
m_func.reset();
}
/// Prevents the function from being invoked when we go out of scope.
__fi void Cancel()
{
m_func.reset();
}
private:
std::optional<T> m_func;
};

187
common/Semaphore.cpp Normal file
View File

@@ -0,0 +1,187 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/Threading.h"
#include "common/Assertions.h"
#include "common/HostSys.h"
#ifdef _WIN32
#include "common/RedtapeWindows.h"
#endif
#include <limits>
// --------------------------------------------------------------------------------------
// Semaphore Implementations
// --------------------------------------------------------------------------------------
bool Threading::WorkSema::CheckForWork()
{
s32 value = m_state.load(std::memory_order_relaxed);
pxAssert(!IsDead(value));
// we want to switch to the running state, but preserve the waiting empty bit for RUNNING_N -> RUNNING_0
// otherwise, we clear the waiting flag (since we're notifying the waiter that we're empty below)
while (!m_state.compare_exchange_weak(value,
IsReadyForSleep(value) ? STATE_RUNNING_0 : (value & STATE_FLAG_WAITING_EMPTY),
std::memory_order_acq_rel, std::memory_order_relaxed))
{
}
// if we're not empty, we have work to do
if (!IsReadyForSleep(value))
return true;
// this means we're empty, so notify any waiters
if (value & STATE_FLAG_WAITING_EMPTY)
m_empty_sema.Post();
// no work to do
return false;
}
void Threading::WorkSema::WaitForWork()
{
// State change:
// SLEEPING, SPINNING: This is the worker thread and it's clearly not asleep or spinning, so these states should be impossible
// RUNNING_0: Change state to SLEEPING, wake up thread if WAITING_EMPTY
// RUNNING_N: Change state to RUNNING_0 (and preserve WAITING_EMPTY flag)
s32 value = m_state.load(std::memory_order_relaxed);
pxAssert(!IsDead(value));
while (!m_state.compare_exchange_weak(value, NextStateWaitForWork(value), std::memory_order_acq_rel, std::memory_order_relaxed))
;
if (IsReadyForSleep(value))
{
if (value & STATE_FLAG_WAITING_EMPTY)
m_empty_sema.Post();
m_sema.Wait();
// Acknowledge any additional work added between wake up request and getting here
m_state.fetch_and(STATE_FLAG_WAITING_EMPTY, std::memory_order_acquire);
}
}
void Threading::WorkSema::WaitForWorkWithSpin()
{
s32 value = m_state.load(std::memory_order_relaxed);
pxAssert(!IsDead(value));
while (IsReadyForSleep(value))
{
if (m_state.compare_exchange_weak(value, STATE_SPINNING, std::memory_order_release, std::memory_order_relaxed))
{
if (value & STATE_FLAG_WAITING_EMPTY)
m_empty_sema.Post();
value = STATE_SPINNING;
break;
}
}
u32 waited = 0;
while (value < 0)
{
if (waited > SPIN_TIME_NS)
{
if (!m_state.compare_exchange_weak(value, STATE_SLEEPING, std::memory_order_relaxed))
continue;
m_sema.Wait();
break;
}
waited += ShortSpin();
value = m_state.load(std::memory_order_relaxed);
}
// Clear back to STATE_RUNNING_0 (but preserve waiting empty flag)
m_state.fetch_and(STATE_FLAG_WAITING_EMPTY, std::memory_order_acquire);
}
bool Threading::WorkSema::WaitForEmpty()
{
s32 value = m_state.load(std::memory_order_acquire);
while (true)
{
if (value < 0)
return !IsDead(value); // STATE_SLEEPING or STATE_SPINNING, queue is empty!
// Note: We technically only need memory_order_acquire on *failure* (because that's when we could leave without sleeping), but libstdc++ still asserts on failure < success
if (m_state.compare_exchange_weak(value, value | STATE_FLAG_WAITING_EMPTY, std::memory_order_acquire))
break;
}
pxAssertMsg(!(value & STATE_FLAG_WAITING_EMPTY), "Multiple threads attempted to wait for empty (not currently supported)");
m_empty_sema.Wait();
return !IsDead(m_state.load(std::memory_order_relaxed));
}
bool Threading::WorkSema::WaitForEmptyWithSpin()
{
s32 value = m_state.load(std::memory_order_acquire);
u32 waited = 0;
while (true)
{
if (value < 0)
return !IsDead(value); // STATE_SLEEPING or STATE_SPINNING, queue is empty!
if (waited > SPIN_TIME_NS && m_state.compare_exchange_weak(value, value | STATE_FLAG_WAITING_EMPTY, std::memory_order_acquire))
break;
waited += ShortSpin();
value = m_state.load(std::memory_order_acquire);
}
pxAssertMsg(!(value & STATE_FLAG_WAITING_EMPTY), "Multiple threads attempted to wait for empty (not currently supported)");
m_empty_sema.Wait();
return !IsDead(m_state.load(std::memory_order_relaxed));
}
void Threading::WorkSema::Kill()
{
s32 value = m_state.exchange(std::numeric_limits<s32>::min(), std::memory_order_release);
if (value & STATE_FLAG_WAITING_EMPTY)
m_empty_sema.Post();
}
void Threading::WorkSema::Reset()
{
m_state = STATE_RUNNING_0;
}
#if !defined(__APPLE__) // macOS implementations are in DarwinThreads
Threading::KernelSemaphore::KernelSemaphore()
{
#ifdef _WIN32
m_sema = CreateSemaphore(nullptr, 0, LONG_MAX, nullptr);
#else
sem_init(&m_sema, false, 0);
#endif
}
Threading::KernelSemaphore::~KernelSemaphore()
{
#ifdef _WIN32
CloseHandle(m_sema);
#else
sem_destroy(&m_sema);
#endif
}
void Threading::KernelSemaphore::Post()
{
#ifdef _WIN32
ReleaseSemaphore(m_sema, 1, nullptr);
#else
sem_post(&m_sema);
#endif
}
void Threading::KernelSemaphore::Wait()
{
#ifdef _WIN32
WaitForSingleObject(m_sema, INFINITE);
#else
sem_wait(&m_sema);
#endif
}
bool Threading::KernelSemaphore::TryWait()
{
#ifdef _WIN32
return WaitForSingleObject(m_sema, 0) == WAIT_OBJECT_0;
#else
return sem_trywait(&m_sema) == 0;
#endif
}
#endif

262
common/SettingsInterface.h Normal file
View File

@@ -0,0 +1,262 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include "SmallString.h"
#include <string>
#include <optional>
#include <vector>
class Error;
class SettingsInterface
{
public:
virtual ~SettingsInterface() = default;
virtual bool Save(Error* error = nullptr) = 0;
virtual void Clear() = 0;
virtual bool IsEmpty() = 0;
virtual bool GetIntValue(const char* section, const char* key, int* value) const = 0;
virtual bool GetUIntValue(const char* section, const char* key, uint* value) const = 0;
virtual bool GetFloatValue(const char* section, const char* key, float* value) const = 0;
virtual bool GetDoubleValue(const char* section, const char* key, double* value) const = 0;
virtual bool GetBoolValue(const char* section, const char* key, bool* value) const = 0;
virtual bool GetStringValue(const char* section, const char* key, std::string* value) const = 0;
virtual bool GetStringValue(const char* section, const char* key, SmallStringBase* value) const = 0;
virtual void SetIntValue(const char* section, const char* key, int value) = 0;
virtual void SetUIntValue(const char* section, const char* key, uint value) = 0;
virtual void SetFloatValue(const char* section, const char* key, float value) = 0;
virtual void SetDoubleValue(const char* section, const char* key, double value) = 0;
virtual void SetBoolValue(const char* section, const char* key, bool value) = 0;
virtual void SetStringValue(const char* section, const char* key, const char* value) = 0;
virtual std::vector<std::string> GetStringList(const char* section, const char* key) const = 0;
virtual void SetStringList(const char* section, const char* key, const std::vector<std::string>& items) = 0;
virtual bool RemoveFromStringList(const char* section, const char* key, const char* item) = 0;
virtual bool AddToStringList(const char* section, const char* key, const char* item) = 0;
virtual std::vector<std::pair<std::string, std::string>> GetKeyValueList(const char* section) const = 0;
virtual void SetKeyValueList(const char* section, const std::vector<std::pair<std::string, std::string>>& items) = 0;
virtual bool ContainsValue(const char* section, const char* key) const = 0;
virtual void DeleteValue(const char* section, const char* key) = 0;
virtual void ClearSection(const char* section) = 0;
virtual void RemoveSection(const char* section) = 0;
virtual void RemoveEmptySections() = 0;
__fi int GetIntValue(const char* section, const char* key, int default_value = 0) const
{
int value;
return GetIntValue(section, key, &value) ? value : default_value;
}
__fi uint GetUIntValue(const char* section, const char* key, uint default_value = 0) const
{
uint value;
return GetUIntValue(section, key, &value) ? value : default_value;
}
__fi float GetFloatValue(const char* section, const char* key, float default_value = 0.0f) const
{
float value;
return GetFloatValue(section, key, &value) ? value : default_value;
}
__fi float GetDoubleValue(const char* section, const char* key, double default_value = 0.0) const
{
double value;
return GetDoubleValue(section, key, &value) ? value : default_value;
}
__fi bool GetBoolValue(const char* section, const char* key, bool default_value = false) const
{
bool value;
return GetBoolValue(section, key, &value) ? value : default_value;
}
__fi std::string GetStringValue(const char* section, const char* key, const char* default_value = "") const
{
std::string value;
if (!GetStringValue(section, key, &value))
value.assign(default_value);
return value;
}
__fi SmallString GetSmallStringValue(const char* section, const char* key, const char* default_value = "") const
{
SmallString value;
if (!GetStringValue(section, key, &value))
value.assign(default_value);
return value;
}
__fi TinyString GetTinyStringValue(const char* section, const char* key, const char* default_value = "") const
{
TinyString value;
if (!GetStringValue(section, key, &value))
value.assign(default_value);
return value;
}
__fi std::optional<int> GetOptionalIntValue(const char* section, const char* key, std::optional<int> default_value = std::nullopt) const
{
int ret;
return GetIntValue(section, key, &ret) ? std::optional<int>(ret) : default_value;
}
__fi std::optional<uint> GetOptionalUIntValue(const char* section, const char* key, std::optional<uint> default_value = std::nullopt) const
{
uint ret;
return GetUIntValue(section, key, &ret) ? std::optional<uint>(ret) : default_value;
}
__fi std::optional<float> GetOptionalFloatValue(const char* section, const char* key, std::optional<float> default_value = std::nullopt) const
{
float ret;
return GetFloatValue(section, key, &ret) ? std::optional<float>(ret) : default_value;
}
__fi std::optional<double> GetOptionalDoubleValue(const char* section, const char* key, std::optional<double> default_value = std::nullopt) const
{
double ret;
return GetDoubleValue(section, key, &ret) ? std::optional<double>(ret) : default_value;
}
__fi std::optional<bool> GetOptionalBoolValue(const char* section, const char* key, std::optional<bool> default_value = std::nullopt) const
{
bool ret;
return GetBoolValue(section, key, &ret) ? std::optional<bool>(ret) : default_value;
}
__fi std::optional<std::string> GetOptionalStringValue(const char* section, const char* key, std::optional<const char*> default_value = std::nullopt) const
{
std::string ret;
return GetStringValue(section, key, &ret) ? std::optional<std::string>(ret) :
(default_value.has_value() ? std::optional<std::string>(default_value.value()) :
std::optional<std::string>());
}
__fi std::optional<SmallString> GetOptionalSmallStringValue(const char* section, const char* key,
std::optional<const char*> default_value = std::nullopt) const
{
SmallString ret;
return GetStringValue(section, key, &ret) ?
std::optional<SmallString>(ret) :
(default_value.has_value() ? std::optional<SmallString>(default_value.value()) :
std::optional<SmallString>());
}
__fi std::optional<TinyString> GetOptionalTinyStringValue(const char* section, const char* key,
std::optional<const char*> default_value = std::nullopt) const
{
TinyString ret;
return GetStringValue(section, key, &ret) ?
std::optional<TinyString>(ret) :
(default_value.has_value() ? std::optional<TinyString>(default_value.value()) :
std::optional<TinyString>());
}
__fi void SetOptionalIntValue(const char* section, const char* key, const std::optional<int>& value)
{
value.has_value() ? SetIntValue(section, key, value.value()) : DeleteValue(section, key);
}
__fi void SetOptionalUIntValue(const char* section, const char* key, const std::optional<uint>& value)
{
value.has_value() ? SetUIntValue(section, key, value.value()) : DeleteValue(section, key);
}
__fi void SetOptionalFloatValue(const char* section, const char* key, const std::optional<float>& value)
{
value.has_value() ? SetFloatValue(section, key, value.value()) : DeleteValue(section, key);
}
__fi void SetOptionalDoubleValue(const char* section, const char* key, const std::optional<double>& value)
{
value.has_value() ? SetDoubleValue(section, key, value.value()) : DeleteValue(section, key);
}
__fi void SetOptionalBoolValue(const char* section, const char* key, const std::optional<bool>& value)
{
value.has_value() ? SetBoolValue(section, key, value.value()) : DeleteValue(section, key);
}
__fi void SetOptionalStringValue(const char* section, const char* key, const std::optional<const char*>& value)
{
value.has_value() ? SetStringValue(section, key, value.value()) : DeleteValue(section, key);
}
__fi void CopyBoolValue(const SettingsInterface& si, const char* section, const char* key)
{
bool value;
if (si.GetBoolValue(section, key, &value))
SetBoolValue(section, key, value);
else
DeleteValue(section, key);
}
__fi void CopyIntValue(const SettingsInterface& si, const char* section, const char* key)
{
int value;
if (si.GetIntValue(section, key, &value))
SetIntValue(section, key, value);
else
DeleteValue(section, key);
}
__fi void CopyUIntValue(const SettingsInterface& si, const char* section, const char* key)
{
uint value;
if (si.GetUIntValue(section, key, &value))
SetUIntValue(section, key, value);
else
DeleteValue(section, key);
}
__fi void CopyFloatValue(const SettingsInterface& si, const char* section, const char* key)
{
float value;
if (si.GetFloatValue(section, key, &value))
SetFloatValue(section, key, value);
else
DeleteValue(section, key);
}
__fi void CopyDoubleValue(const SettingsInterface& si, const char* section, const char* key)
{
double value;
if (si.GetDoubleValue(section, key, &value))
SetDoubleValue(section, key, value);
else
DeleteValue(section, key);
}
__fi void CopyStringValue(const SettingsInterface& si, const char* section, const char* key)
{
std::string value;
if (si.GetStringValue(section, key, &value))
SetStringValue(section, key, value.c_str());
else
DeleteValue(section, key);
}
__fi void CopyStringListValue(const SettingsInterface& si, const char* section, const char* key)
{
std::vector<std::string> value(si.GetStringList(section, key));
if (!value.empty())
SetStringList(section, key, value);
else
DeleteValue(section, key);
}
__fi void CopyKeysAndValues(const SettingsInterface& si, const char* section)
{
SetKeyValueList(section, si.GetKeyValueList(section));
}
};

230
common/SettingsWrapper.cpp Normal file
View File

@@ -0,0 +1,230 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include <algorithm>
#include "SettingsWrapper.h"
#include "Console.h"
static int _calcEnumLength(const char* const* enumArray)
{
int cnt = 0;
while (*enumArray != nullptr)
{
enumArray++;
cnt++;
}
return cnt;
}
SettingsWrapper::SettingsWrapper(SettingsInterface& si)
: m_si(si)
{
}
SettingsLoadWrapper::SettingsLoadWrapper(SettingsInterface& si)
: SettingsWrapper(si)
{
}
bool SettingsLoadWrapper::IsLoading() const
{
return true;
}
bool SettingsLoadWrapper::IsSaving() const
{
return false;
}
void SettingsLoadWrapper::Entry(const char* section, const char* var, int& value, const int defvalue /*= 0*/)
{
value = m_si.GetIntValue(section, var, defvalue);
}
void SettingsLoadWrapper::Entry(const char* section, const char* var, uint& value, const uint defvalue /*= 0*/)
{
value = m_si.GetUIntValue(section, var, defvalue);
}
void SettingsLoadWrapper::Entry(const char* section, const char* var, bool& value, const bool defvalue /*= false*/)
{
value = m_si.GetBoolValue(section, var, defvalue);
}
void SettingsLoadWrapper::Entry(const char* section, const char* var, float& value, const float defvalue /*= 0.0*/)
{
value = m_si.GetFloatValue(section, var, defvalue);
}
void SettingsLoadWrapper::Entry(const char* section, const char* var, std::string& value, const std::string& default_value /*= std::string()*/)
{
if (!m_si.GetStringValue(section, var, &value) && &value != &default_value)
value = default_value;
}
void SettingsLoadWrapper::Entry(const char* section, const char* var, SmallStringBase& value, std::string_view default_value /* = std::string_view() */)
{
if (!m_si.GetStringValue(section, var, &value) && value.data() != default_value.data())
value = default_value;
}
void SettingsLoadWrapper::_EnumEntry(const char* section, const char* var, int& value, const char* const* enumArray, int defvalue)
{
const int cnt = _calcEnumLength(enumArray);
defvalue = std::clamp(defvalue, 0, cnt);
const std::string retval(m_si.GetStringValue(section, var, enumArray[defvalue]));
int i = 0;
while (enumArray[i] != nullptr && (retval != enumArray[i]))
i++;
if (enumArray[i] == nullptr)
{
Console.Warning("(LoadSettings) Warning: Unrecognized value '%s' on key '%s'\n\tUsing the default setting of '%s'.",
retval.c_str(), var, enumArray[defvalue]);
value = defvalue;
}
else
{
value = i;
}
}
bool SettingsLoadWrapper::EntryBitBool(const char* section, const char* var, bool value, const bool defvalue /*= false*/)
{
return m_si.GetBoolValue(section, var, defvalue);
}
int SettingsLoadWrapper::EntryBitfield(const char* section, const char* var, int value, const int defvalue /*= 0*/)
{
return m_si.GetIntValue(section, var, defvalue);
}
SettingsSaveWrapper::SettingsSaveWrapper(SettingsInterface& si)
: SettingsWrapper(si)
{
}
bool SettingsSaveWrapper::IsLoading() const
{
return false;
}
bool SettingsSaveWrapper::IsSaving() const
{
return true;
}
void SettingsSaveWrapper::Entry(const char* section, const char* var, int& value, const int defvalue /*= 0*/)
{
m_si.SetIntValue(section, var, value);
}
void SettingsSaveWrapper::Entry(const char* section, const char* var, uint& value, const uint defvalue /*= 0*/)
{
m_si.SetUIntValue(section, var, value);
}
void SettingsSaveWrapper::Entry(const char* section, const char* var, bool& value, const bool defvalue /*= false*/)
{
m_si.SetBoolValue(section, var, value);
}
void SettingsSaveWrapper::Entry(const char* section, const char* var, float& value, const float defvalue /*= 0.0*/)
{
m_si.SetFloatValue(section, var, value);
}
void SettingsSaveWrapper::Entry(const char* section, const char* var, std::string& value, const std::string& default_value /*= std::string()*/)
{
m_si.SetStringValue(section, var, value.c_str());
}
void SettingsSaveWrapper::Entry(const char* section, const char* var, SmallStringBase& value, std::string_view default_value /* = std::string_view() */)
{
m_si.SetStringValue(section, var, value.c_str());
}
bool SettingsSaveWrapper::EntryBitBool(const char* section, const char* var, bool value, const bool defvalue /*= false*/)
{
m_si.SetBoolValue(section, var, value);
return value;
}
int SettingsSaveWrapper::EntryBitfield(const char* section, const char* var, int value, const int defvalue /*= 0*/)
{
m_si.SetIntValue(section, var, value);
return value;
}
void SettingsSaveWrapper::_EnumEntry(const char* section, const char* var, int& value, const char* const* enumArray, int defvalue)
{
const int cnt = _calcEnumLength(enumArray);
const int index = (value < 0 || value >= cnt) ? defvalue : value;
m_si.SetStringValue(section, var, enumArray[index]);
}
SettingsClearWrapper::SettingsClearWrapper(SettingsInterface& si)
: SettingsWrapper(si)
{
}
bool SettingsClearWrapper::IsLoading() const
{
return false;
}
bool SettingsClearWrapper::IsSaving() const
{
return true;
}
void SettingsClearWrapper::Entry(const char* section, const char* var, int& value, const int defvalue /*= 0*/)
{
m_si.DeleteValue(section, var);
}
void SettingsClearWrapper::Entry(const char* section, const char* var, uint& value, const uint defvalue /*= 0*/)
{
m_si.DeleteValue(section, var);
}
void SettingsClearWrapper::Entry(const char* section, const char* var, bool& value, const bool defvalue /*= false*/)
{
m_si.DeleteValue(section, var);
}
void SettingsClearWrapper::Entry(const char* section, const char* var, float& value, const float defvalue /*= 0.0*/)
{
m_si.DeleteValue(section, var);
}
void SettingsClearWrapper::Entry(const char* section, const char* var, std::string& value, const std::string& default_value /*= std::string()*/)
{
m_si.DeleteValue(section, var);
}
void SettingsClearWrapper::Entry(const char* section, const char* var, SmallStringBase& value, std::string_view default_value /* = std::string_view() */)
{
m_si.DeleteValue(section, var);
}
bool SettingsClearWrapper::EntryBitBool(const char* section, const char* var, bool value, const bool defvalue /*= false*/)
{
m_si.DeleteValue(section, var);
return defvalue;
}
int SettingsClearWrapper::EntryBitfield(const char* section, const char* var, int value, const int defvalue /*= 0*/)
{
m_si.DeleteValue(section, var);
return defvalue;
}
void SettingsClearWrapper::_EnumEntry(const char* section, const char* var, int& value, const char* const* enumArray, int defvalue)
{
m_si.DeleteValue(section, var);
}

137
common/SettingsWrapper.h Normal file
View File

@@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "SettingsInterface.h"
#include "common/EnumOps.h"
#include "common/SmallString.h"
#include <optional>
// Helper class which loads or saves depending on the derived class.
class SettingsWrapper
{
public:
SettingsWrapper(SettingsInterface& si);
virtual bool IsLoading() const = 0;
virtual bool IsSaving() const = 0;
virtual void Entry(const char* section, const char* var, int& value, const int defvalue = 0) = 0;
virtual void Entry(const char* section, const char* var, uint& value, const uint defvalue = 0) = 0;
virtual void Entry(const char* section, const char* var, bool& value, const bool defvalue = false) = 0;
virtual void Entry(const char* section, const char* var, float& value, const float defvalue = 0.0) = 0;
virtual void Entry(const char* section, const char* var, std::string& value, const std::string& default_value = std::string()) = 0;
virtual void Entry(const char* section, const char* var, SmallStringBase& value, std::string_view default_value = std::string_view()) = 0;
// This special form of Entry is provided for bitfields, which cannot be passed by reference.
virtual bool EntryBitBool(const char* section, const char* var, bool value, const bool defvalue = false) = 0;
virtual int EntryBitfield(const char* section, const char* var, int value, const int defvalue = 0) = 0;
template <typename T>
void EnumEntry(const char* section, const char* var, T& value, const char* const* enumArray = nullptr, const T defvalue = (T)0)
{
int tstore = (int)value;
auto defaultvalue = enum_cast(defvalue);
if (enumArray == NULL)
Entry(section, var, tstore, defaultvalue);
else
_EnumEntry(section, var, tstore, enumArray, defaultvalue);
value = (T)tstore;
}
template <typename T>
void EnumEntry(const char* section, const char* var, T& value, std::optional<T> (*parse_function)(const char*),
const char*(name_function)(T value), T default_value)
{
TinyString str_value(name_function(value));
Entry(section, var, str_value, name_function(default_value));
if (std::optional<T> parsed_value = parse_function(str_value); parsed_value.has_value())
value = parsed_value.value();
}
protected:
virtual void _EnumEntry(const char* section, const char* var, int& value, const char* const* enumArray, int defvalue) = 0;
SettingsInterface& m_si;
};
class SettingsLoadWrapper final : public SettingsWrapper
{
public:
SettingsLoadWrapper(SettingsInterface& si);
bool IsLoading() const override;
bool IsSaving() const override;
void Entry(const char* section, const char* var, int& value, const int defvalue = 0) override;
void Entry(const char* section, const char* var, uint& value, const uint defvalue = 0) override;
void Entry(const char* section, const char* var, bool& value, const bool defvalue = false) override;
void Entry(const char* section, const char* var, float& value, const float defvalue = 0.0) override;
void Entry(const char* section, const char* var, std::string& value, const std::string& default_value = std::string()) override;
void Entry(const char* section, const char* var, SmallStringBase& value, std::string_view default_value = std::string_view()) override;
bool EntryBitBool(const char* section, const char* var, bool value, const bool defvalue = false) override;
int EntryBitfield(const char* section, const char* var, int value, const int defvalue = 0) override;
protected:
void _EnumEntry(const char* section, const char* var, int& value, const char* const* enumArray, int defvalue) override;
};
class SettingsSaveWrapper final : public SettingsWrapper
{
public:
SettingsSaveWrapper(SettingsInterface& si);
bool IsLoading() const override;
bool IsSaving() const override;
void Entry(const char* section, const char* var, int& value, const int defvalue = 0) override;
void Entry(const char* section, const char* var, uint& value, const uint defvalue = 0) override;
void Entry(const char* section, const char* var, bool& value, const bool defvalue = false) override;
void Entry(const char* section, const char* var, float& value, const float defvalue = 0.0) override;
void Entry(const char* section, const char* var, std::string& value, const std::string& default_value = std::string()) override;
void Entry(const char* section, const char* var, SmallStringBase& value, std::string_view default_value = std::string_view()) override;
bool EntryBitBool(const char* section, const char* var, bool value, const bool defvalue = false) override;
int EntryBitfield(const char* section, const char* var, int value, const int defvalue = 0) override;
protected:
void _EnumEntry(const char* section, const char* var, int& value, const char* const* enumArray, int defvalue) override;
};
class SettingsClearWrapper final : public SettingsWrapper
{
public:
SettingsClearWrapper(SettingsInterface& si);
bool IsLoading() const override;
bool IsSaving() const override;
void Entry(const char* section, const char* var, int& value, const int defvalue = 0) override;
void Entry(const char* section, const char* var, uint& value, const uint defvalue = 0) override;
void Entry(const char* section, const char* var, bool& value, const bool defvalue = false) override;
void Entry(const char* section, const char* var, float& value, const float defvalue = 0.0) override;
void Entry(const char* section, const char* var, std::string& value, const std::string& default_value = std::string()) override;
void Entry(const char* section, const char* var, SmallStringBase& value, std::string_view default_value = std::string_view()) override;
bool EntryBitBool(const char* section, const char* var, bool value, const bool defvalue = false) override;
int EntryBitfield(const char* section, const char* var, int value, const int defvalue = 0) override;
protected:
void _EnumEntry(const char* section, const char* var, int& value, const char* const* enumArray, int defvalue) override;
};
#define SettingsWrapSection(section) const char* CURRENT_SETTINGS_SECTION = section;
#define SettingsWrapEntry(var) wrap.Entry(CURRENT_SETTINGS_SECTION, #var, var, var)
#define SettingsWrapEntryEx(var, name) wrap.Entry(CURRENT_SETTINGS_SECTION, name, var, var)
#define SettingsWrapBitfield(varname) varname = wrap.EntryBitfield(CURRENT_SETTINGS_SECTION, #varname, varname, varname)
#define SettingsWrapBitBool(varname) varname = wrap.EntryBitBool(CURRENT_SETTINGS_SECTION, #varname, !!varname, varname)
#define SettingsWrapBitfieldEx(varname, textname) varname = wrap.EntryBitfield(CURRENT_SETTINGS_SECTION, textname, varname, varname)
#define SettingsWrapBitBoolEx(varname, textname) varname = wrap.EntryBitBool(CURRENT_SETTINGS_SECTION, textname, !!varname, varname)
#define SettingsWrapEnumEx(varname, textname, names) wrap.EnumEntry(CURRENT_SETTINGS_SECTION, textname, varname, names, varname)
#define SettingsWrapParsedEnum(varname, textname, parse_func, name_func) wrap.EnumEntry(CURRENT_SETTINGS_SECTION, textname, varname, parse_func, name_func, varname)
#define SettingsWrapIntEnumEx(varname, textname) varname = static_cast<decltype(varname)>(wrap.EntryBitfield(CURRENT_SETTINGS_SECTION, textname, static_cast<int>(varname), static_cast<int>(varname)))

View File

@@ -0,0 +1,195 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// --------------------------------------------------------------------------------------
// r64 / r128 - Types that are guaranteed to fit in one register
// --------------------------------------------------------------------------------------
// Note: Recompilers rely on some of these types and the registers they allocate to,
// so be careful if you want to change them
#pragma once
#include "Pcsx2Defs.h"
#include "Pcsx2Types.h"
#include "VectorIntrin.h"
#include <cstring>
#if defined(_M_X86)
// Can't stick them in structs because it breaks calling convention things, yay
using r128 = __m128i;
// Calling convention setting, yay
#define RETURNS_R128 r128 __vectorcall
#define TAKES_R128 __vectorcall
// And since we can't stick them in structs, we get lots of static methods, yay!
[[maybe_unused]] __fi static r128 r128_load(const void* ptr)
{
return _mm_load_si128(reinterpret_cast<const r128*>(ptr));
}
[[maybe_unused]] __fi static void r128_store(void* ptr, r128 val)
{
return _mm_store_si128(reinterpret_cast<r128*>(ptr), val);
}
[[maybe_unused]] __fi static void r128_store_unaligned(void* ptr, r128 val)
{
return _mm_storeu_si128(reinterpret_cast<r128*>(ptr), val);
}
[[maybe_unused]] __fi static r128 r128_zero()
{
return _mm_setzero_si128();
}
/// Expects that r64 came from r64-handling code, and not from a recompiler or something
[[maybe_unused]] __fi static r128 r128_from_u64_dup(u64 val)
{
return _mm_set1_epi64x(val);
}
[[maybe_unused]] __fi static r128 r128_from_u64_zext(u64 val)
{
return _mm_set_epi64x(0, val);
}
[[maybe_unused]] __fi static r128 r128_from_u32_dup(u32 val)
{
return _mm_set1_epi32(val);
}
[[maybe_unused]] __fi static r128 r128_from_u32x4(u32 lo0, u32 lo1, u32 hi0, u32 hi1)
{
return _mm_setr_epi32(lo0, lo1, hi0, hi1);
}
[[maybe_unused]] __fi static r128 r128_from_u128(const u128& u)
{
return _mm_loadu_si128(reinterpret_cast<const __m128i*>(&u));
}
[[maybe_unused]] __fi static u32 r128_to_u32(r128 val)
{
return _mm_cvtsi128_si32(val);
}
[[maybe_unused]] __fi static u64 r128_to_u64(r128 val)
{
return _mm_cvtsi128_si64(val);
}
[[maybe_unused]] __fi static u128 r128_to_u128(r128 val)
{
alignas(16) u128 ret;
_mm_store_si128(reinterpret_cast<r128*>(&ret), val);
return ret;
}
[[maybe_unused]] __fi static void CopyQWC(void* dest, const void* src)
{
_mm_store_ps((float*)dest, _mm_load_ps((const float*)src));
}
[[maybe_unused]] __fi static void ZeroQWC(void* dest)
{
_mm_store_ps((float*)dest, _mm_setzero_ps());
}
[[maybe_unused]] __fi static void ZeroQWC(u128& dest)
{
_mm_store_ps((float*)&dest, _mm_setzero_ps());
}
#elif defined(_M_ARM64)
using r128 = uint32x4_t;
#define RETURNS_R128 r128 __vectorcall
#define TAKES_R128 __vectorcall
[[maybe_unused]] __fi static void CopyQWC(void* dest, const void* src)
{
vst1q_u8(static_cast<u8*>(dest), vld1q_u8(static_cast<const u8*>(src)));
}
[[maybe_unused]] __fi static void ZeroQWC(void* dest)
{
vst1q_u8(static_cast<u8*>(dest), vmovq_n_u8(0));
}
[[maybe_unused]] __fi static void ZeroQWC(u128& dest)
{
vst1q_u8(&dest._u8[0], vmovq_n_u8(0));
}
[[maybe_unused]] __fi static r128 r128_load(const void* ptr)
{
return vld1q_u32(reinterpret_cast<const uint32_t*>(ptr));
}
[[maybe_unused]] __fi static void r128_store(void* ptr, r128 value)
{
return vst1q_u32(reinterpret_cast<uint32_t*>(ptr), value);
}
[[maybe_unused]] __fi static void r128_store_unaligned(void* ptr, r128 value)
{
return vst1q_u32(reinterpret_cast<uint32_t*>(ptr), value);
}
[[maybe_unused]] __fi static r128 r128_zero()
{
return vmovq_n_u32(0);
}
/// Expects that r64 came from r64-handling code, and not from a recompiler or something
[[maybe_unused]] __fi static r128 r128_from_u64_dup(u64 val)
{
return vreinterpretq_u32_u64(vdupq_n_u64(val));
}
[[maybe_unused]] __fi static r128 r128_from_u64_zext(u64 val)
{
return vreinterpretq_u32_u64(vcombine_u64(vcreate_u64(val), vcreate_u64(0)));
}
[[maybe_unused]] __fi static r128 r128_from_u32_dup(u32 val)
{
return vdupq_n_u32(val);
}
[[maybe_unused]] __fi static r128 r128_from_u32x4(u32 lo0, u32 lo1, u32 hi0, u32 hi1)
{
const u32 values[4] = {lo0, lo1, hi0, hi1};
return vld1q_u32(values);
}
[[maybe_unused]] __fi static r128 r128_from_u128(const u128& u)
{
return vld1q_u32(reinterpret_cast<const uint32_t*>(u._u32));
}
[[maybe_unused]] __fi static u32 r128_to_u32(r128 val)
{
return vgetq_lane_u32(val, 0);
}
[[maybe_unused]] __fi static u64 r128_to_u64(r128 val)
{
return vgetq_lane_u64(vreinterpretq_u64_u32(val), 0);
}
[[maybe_unused]] __fi static u128 r128_to_u128(r128 val)
{
alignas(16) u128 ret;
vst1q_u32(ret._u32, val);
return ret;
}
#else
#error Unknown architecture.
#endif

843
common/SmallString.cpp Normal file
View File

@@ -0,0 +1,843 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "SmallString.h"
#include "Assertions.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#ifdef _MSC_VER
#define CASE_COMPARE _stricmp
#define CASE_N_COMPARE _strnicmp
#else
#define CASE_COMPARE strcasecmp
#define CASE_N_COMPARE strncasecmp
#endif
SmallStringBase::SmallStringBase() = default;
SmallStringBase::SmallStringBase(const SmallStringBase& copy)
{
assign(copy.m_buffer, copy.m_length);
}
SmallStringBase::SmallStringBase(const char* str)
{
assign(str);
}
SmallStringBase::SmallStringBase(const char* str, u32 count)
{
assign(str, count);
}
SmallStringBase::SmallStringBase(SmallStringBase&& move)
{
assign(std::move(move));
}
SmallStringBase::SmallStringBase(const std::string_view sv)
{
assign(sv);
}
SmallStringBase::SmallStringBase(const std::string& str)
{
assign(str);
}
SmallStringBase::~SmallStringBase()
{
if (m_on_heap)
std::free(m_buffer);
}
void SmallStringBase::reserve(u32 new_reserve)
{
const u32 real_reserve = new_reserve + 1;
if (m_buffer_size >= real_reserve)
return;
if (m_on_heap)
{
char* new_ptr = static_cast<char*>(std::realloc(m_buffer, real_reserve));
if (!new_ptr)
pxFailRel("Memory allocation failed.");
#ifdef _DEBUG
std::memset(new_ptr + m_length, 0, real_reserve - m_length);
#endif
m_buffer = new_ptr;
}
else
{
char* new_ptr = static_cast<char*>(std::malloc(real_reserve));
if (!new_ptr)
pxFailRel("Memory allocation failed.");
if (m_length > 0)
std::memcpy(new_ptr, m_buffer, m_length);
#ifdef _DEBUG
std::memset(new_ptr + m_length, 0, real_reserve - m_length);
#else
new_ptr[m_length] = 0;
#endif
m_buffer = new_ptr;
m_on_heap = true;
}
m_buffer_size = new_reserve;
}
void SmallStringBase::shrink_to_fit()
{
const u32 buffer_size = (m_length + 1);
if (!m_on_heap || buffer_size == m_buffer_size)
return;
if (m_length == 0)
{
std::free(m_buffer);
m_buffer = nullptr;
m_buffer_size = 0;
return;
}
char* new_ptr = static_cast<char*>(std::realloc(m_buffer, buffer_size));
if (!new_ptr)
pxFailRel("Memory allocation failed.");
m_buffer = new_ptr;
m_buffer_size = buffer_size;
}
std::string_view SmallStringBase::view() const
{
return (m_length == 0) ? std::string_view() : std::string_view(m_buffer, m_length);
}
SmallStringBase& SmallStringBase::operator=(SmallStringBase&& move)
{
assign(move);
return *this;
}
SmallStringBase& SmallStringBase::operator=(const std::string_view str)
{
assign(str);
return *this;
}
SmallStringBase& SmallStringBase::operator=(const std::string& str)
{
assign(str);
return *this;
}
SmallStringBase& SmallStringBase::operator=(const char* str)
{
assign(str);
return *this;
}
SmallStringBase& SmallStringBase::operator=(const SmallStringBase& copy)
{
assign(copy);
return *this;
}
void SmallStringBase::make_room_for(u32 space)
{
const u32 required_size = m_length + space + 1;
if (m_buffer_size >= required_size)
return;
reserve(std::max(required_size, m_buffer_size * 2));
}
void SmallStringBase::append(const char* str, u32 length)
{
if (length == 0)
return;
make_room_for(length);
pxAssert((length + m_length) < m_buffer_size);
std::memcpy(m_buffer + m_length, str, length);
m_length += length;
m_buffer[m_length] = 0;
}
void SmallStringBase::append_hex(const void* data, size_t len)
{
if (len == 0)
return;
make_room_for(static_cast<u32>(len) * 4);
const u8* bytes = static_cast<const u8*>(data);
append_format("{:02X}", bytes[0]);
for (size_t i = 1; i < len; i++)
append_format(", {:02X}", bytes[i]);
}
void SmallStringBase::prepend(const char* str, u32 length)
{
if (length == 0)
return;
make_room_for(length);
pxAssert((length + m_length) < m_buffer_size);
std::memmove(m_buffer + length, m_buffer, m_length);
std::memcpy(m_buffer, str, length);
m_length += length;
m_buffer[m_length] = 0;
}
void SmallStringBase::append(char c)
{
append(&c, 1);
}
void SmallStringBase::append(const SmallStringBase& str)
{
append(str.m_buffer, str.m_length);
}
void SmallStringBase::append(const char* str)
{
append(str, static_cast<u32>(std::strlen(str)));
}
void SmallStringBase::append(const std::string& str)
{
append(str.c_str(), static_cast<u32>(str.length()));
}
void SmallStringBase::append(const std::string_view str)
{
append(str.data(), static_cast<u32>(str.length()));
}
void SmallStringBase::append_sprintf(const char* format, ...)
{
std::va_list ap;
va_start(ap, format);
append_vsprintf(format, ap);
va_end(ap);
}
void SmallStringBase::append_vsprintf(const char* format, va_list ap)
{
// We have a 1KB byte buffer on the stack here. If this is too little, we'll grow it via the heap,
// but 1KB should be enough for most strings.
char stack_buffer[1024];
char* heap_buffer = nullptr;
char* buffer = stack_buffer;
u32 buffer_size = static_cast<u32>(std::size(stack_buffer));
u32 written;
for (;;)
{
std::va_list ap_copy;
va_copy(ap_copy, ap);
const int ret = std::vsnprintf(buffer, buffer_size, format, ap_copy);
va_end(ap_copy);
if (ret < 0 || ((u32)ret >= (buffer_size - 1)))
{
buffer_size *= 2;
buffer = heap_buffer = reinterpret_cast<char*>(std::realloc(heap_buffer, buffer_size));
continue;
}
written = static_cast<u32>(ret);
break;
}
append(buffer, written);
if (heap_buffer)
std::free(heap_buffer);
}
void SmallStringBase::prepend(char c)
{
prepend(&c, 1);
}
void SmallStringBase::prepend(const SmallStringBase& str)
{
prepend(str.m_buffer, str.m_length);
}
void SmallStringBase::prepend(const char* str)
{
prepend(str, static_cast<u32>(std::strlen(str)));
}
void SmallStringBase::prepend(const std::string& str)
{
prepend(str.c_str(), static_cast<u32>(str.length()));
}
void SmallStringBase::prepend(const std::string_view str)
{
prepend(str.data(), static_cast<u32>(str.length()));
}
void SmallStringBase::prepend_sprintf(const char* format, ...)
{
va_list ap;
va_start(ap, format);
prepend_vsprintf(format, ap);
va_end(ap);
}
void SmallStringBase::prepend_vsprintf(const char* format, va_list ArgPtr)
{
// We have a 1KB byte buffer on the stack here. If this is too little, we'll grow it via the heap,
// but 1KB should be enough for most strings.
char stack_buffer[1024];
char* heap_buffer = NULL;
char* buffer = stack_buffer;
u32 buffer_size = static_cast<u32>(std::size(stack_buffer));
u32 written;
for (;;)
{
int ret = std::vsnprintf(buffer, buffer_size, format, ArgPtr);
if (ret < 0 || (static_cast<u32>(ret) >= (buffer_size - 1)))
{
buffer_size *= 2;
buffer = heap_buffer = reinterpret_cast<char*>(std::realloc(heap_buffer, buffer_size));
continue;
}
written = static_cast<u32>(ret);
break;
}
prepend(buffer, written);
if (heap_buffer)
std::free(heap_buffer);
}
void SmallStringBase::insert(s32 offset, const char* str)
{
insert(offset, str, static_cast<u32>(std::strlen(str)));
}
void SmallStringBase::insert(s32 offset, const SmallStringBase& str)
{
insert(offset, str, str.m_length);
}
void SmallStringBase::insert(s32 offset, const char* str, u32 length)
{
if (length == 0)
return;
make_room_for(length);
// calc real offset
u32 real_offset;
if (offset < 0)
real_offset = static_cast<u32>(std::max<s32>(0, static_cast<s32>(m_length) + offset));
else
real_offset = std::min(static_cast<u32>(offset), m_length);
// determine number of characters after offset
pxAssert(real_offset <= m_length);
const u32 chars_after_offset = m_length - real_offset;
if (chars_after_offset > 0)
std::memmove(m_buffer + offset + length, m_buffer + offset, chars_after_offset);
// insert the string
std::memcpy(m_buffer + real_offset, str, length);
m_length += length;
// ensure null termination
m_buffer[m_length] = 0;
}
void SmallStringBase::insert(s32 offset, const std::string& str)
{
insert(offset, str.c_str(), static_cast<u32>(str.size()));
}
void SmallStringBase::insert(s32 offset, const std::string_view str)
{
insert(offset, str.data(), static_cast<u32>(str.size()));
}
void SmallStringBase::sprintf(const char* format, ...)
{
va_list ap;
va_start(ap, format);
vsprintf(format, ap);
va_end(ap);
}
void SmallStringBase::vsprintf(const char* format, va_list ap)
{
clear();
append_vsprintf(format, ap);
}
void SmallStringBase::assign(const SmallStringBase& copy)
{
assign(copy.c_str(), copy.length());
}
void SmallStringBase::assign(const char* str)
{
assign(str, static_cast<u32>(std::strlen(str)));
}
void SmallStringBase::assign(const char* str, u32 length)
{
clear();
if (length > 0)
append(str, length);
}
void SmallStringBase::assign(SmallStringBase&& move)
{
if (move.m_on_heap)
{
if (m_on_heap)
std::free(m_buffer);
m_buffer = move.m_buffer;
m_buffer_size = move.m_buffer_size;
m_length = move.m_length;
m_on_heap = true;
move.m_buffer = nullptr;
move.m_buffer_size = 0;
move.m_length = 0;
}
else
{
assign(move.m_buffer, move.m_buffer_size);
}
}
void SmallStringBase::assign(const std::string& str)
{
clear();
append(str.data(), static_cast<u32>(str.size()));
}
void SmallStringBase::assign(const std::string_view str)
{
clear();
append(str.data(), static_cast<u32>(str.size()));
}
void SmallStringBase::vformat(fmt::string_view fmt, fmt::format_args args)
{
clear();
fmt::vformat_to(std::back_inserter(*this), fmt, args);
}
bool SmallStringBase::equals(const char* str) const
{
if (m_length == 0)
return (std::strlen(str) == 0);
else
return (std::strcmp(m_buffer, str) == 0);
}
bool SmallStringBase::equals(const SmallStringBase& str) const
{
return (m_length == str.m_length && (m_length == 0 || std::strcmp(m_buffer, str.m_buffer) == 0));
}
bool SmallStringBase::equals(const std::string_view str) const
{
return (m_length == static_cast<u32>(str.length()) &&
(m_length == 0 || std::memcmp(m_buffer, str.data(), m_length) == 0));
}
bool SmallStringBase::equals(const std::string& str) const
{
return (m_length == static_cast<u32>(str.length()) &&
(m_length == 0 || std::memcmp(m_buffer, str.data(), m_length) == 0));
}
bool SmallStringBase::iequals(const char* otherText) const
{
if (m_length == 0)
return (std::strlen(otherText) == 0);
else
return (CASE_COMPARE(m_buffer, otherText) == 0);
}
bool SmallStringBase::iequals(const SmallStringBase& str) const
{
return (m_length == str.m_length && (m_length == 0 || std::strcmp(m_buffer, str.m_buffer) == 0));
}
bool SmallStringBase::iequals(const std::string_view str) const
{
return (m_length == static_cast<u32>(str.length()) &&
(m_length == 0 || CASE_N_COMPARE(m_buffer, str.data(), m_length) == 0));
}
bool SmallStringBase::iequals(const std::string& str) const
{
return (m_length == static_cast<u32>(str.length()) &&
(m_length == 0 || CASE_N_COMPARE(m_buffer, str.data(), m_length) == 0));
}
int SmallStringBase::compare(const char* otherText) const
{
return compare(std::string_view(otherText));
}
int SmallStringBase::compare(const SmallStringBase& str) const
{
if (m_length == 0)
return (str.m_length == 0) ? 0 : -1;
else if (str.m_length == 0)
return 1;
const int res = std::strncmp(m_buffer, str.m_buffer, std::min(m_length, str.m_length));
if (m_length == str.m_length || res != 0)
return res;
else
return (m_length > str.m_length) ? 1 : -1;
}
int SmallStringBase::compare(const std::string_view str) const
{
const u32 slength = static_cast<u32>(str.length());
if (m_length == 0)
return (slength == 0) ? 0 : -1;
else if (slength == 0)
return 1;
const int res = std::strncmp(m_buffer, str.data(), std::min(m_length, slength));
if (m_length == slength || res != 0)
return res;
else
return (m_length > slength) ? 1 : -1;
}
int SmallStringBase::compare(const std::string& str) const
{
const u32 slength = static_cast<u32>(str.length());
if (m_length == 0)
return (slength == 0) ? 0 : -1;
else if (slength == 0)
return 1;
const int res = std::strncmp(m_buffer, str.data(), std::min(m_length, slength));
if (m_length == slength || res != 0)
return res;
else
return (m_length > slength) ? 1 : -1;
}
int SmallStringBase::icompare(const char* otherText) const
{
return icompare(std::string_view(otherText));
}
int SmallStringBase::icompare(const SmallStringBase& str) const
{
if (m_length == 0)
return (str.m_length == 0) ? 0 : -1;
else if (str.m_length == 0)
return 1;
const int res = CASE_N_COMPARE(m_buffer, str.m_buffer, std::min(m_length, str.m_length));
if (m_length == str.m_length || res != 0)
return res;
else
return (m_length > str.m_length) ? 1 : -1;
}
int SmallStringBase::icompare(const std::string_view str) const
{
const u32 slength = static_cast<u32>(str.length());
if (m_length == 0)
return (slength == 0) ? 0 : -1;
else if (slength == 0)
return 1;
const int res = CASE_N_COMPARE(m_buffer, str.data(), std::min(m_length, slength));
if (m_length == slength || res != 0)
return res;
else
return (m_length > slength) ? 1 : -1;
}
int SmallStringBase::icompare(const std::string& str) const
{
const u32 slength = static_cast<u32>(str.length());
if (m_length == 0)
return (slength == 0) ? 0 : -1;
else if (slength == 0)
return 1;
const int res = CASE_N_COMPARE(m_buffer, str.data(), std::min(m_length, slength));
if (m_length == slength || res != 0)
return res;
else
return (m_length > slength) ? 1 : -1;
}
bool SmallStringBase::starts_with(const char* str, bool case_sensitive) const
{
const u32 other_length = static_cast<u32>(std::strlen(str));
if (other_length > m_length)
return false;
return (case_sensitive) ? (std::strncmp(str, m_buffer, other_length) == 0) :
(CASE_N_COMPARE(str, m_buffer, other_length) == 0);
}
bool SmallStringBase::starts_with(const SmallStringBase& str, bool case_sensitive) const
{
const u32 other_length = str.m_length;
if (other_length > m_length)
return false;
return (case_sensitive) ? (std::strncmp(str.m_buffer, m_buffer, other_length) == 0) :
(CASE_N_COMPARE(str.m_buffer, m_buffer, other_length) == 0);
}
bool SmallStringBase::starts_with(const std::string_view str, bool case_sensitive) const
{
const u32 other_length = static_cast<u32>(str.length());
if (other_length > m_length)
return false;
return (case_sensitive) ? (std::strncmp(str.data(), m_buffer, other_length) == 0) :
(CASE_N_COMPARE(str.data(), m_buffer, other_length) == 0);
}
bool SmallStringBase::starts_with(const std::string& str, bool case_sensitive) const
{
const u32 other_length = static_cast<u32>(str.length());
if (other_length > m_length)
return false;
return (case_sensitive) ? (std::strncmp(str.data(), m_buffer, other_length) == 0) :
(CASE_N_COMPARE(str.data(), m_buffer, other_length) == 0);
}
bool SmallStringBase::ends_with(const char* str, bool case_sensitive) const
{
const u32 other_length = static_cast<u32>(std::strlen(str));
if (other_length > m_length)
return false;
u32 start_offset = m_length - other_length;
return (case_sensitive) ? (std::strncmp(str, m_buffer + start_offset, other_length) == 0) :
(CASE_N_COMPARE(str, m_buffer + start_offset, other_length) == 0);
}
bool SmallStringBase::ends_with(const SmallStringBase& str, bool case_sensitive) const
{
const u32 other_length = str.m_length;
if (other_length > m_length)
return false;
const u32 start_offset = m_length - other_length;
return (case_sensitive) ? (std::strncmp(str.m_buffer, m_buffer + start_offset, other_length) == 0) :
(CASE_N_COMPARE(str.m_buffer, m_buffer + start_offset, other_length) == 0);
}
bool SmallStringBase::ends_with(const std::string_view str, bool case_sensitive) const
{
const u32 other_length = static_cast<u32>(str.length());
if (other_length > m_length)
return false;
const u32 start_offset = m_length - other_length;
return (case_sensitive) ? (std::strncmp(str.data(), m_buffer + start_offset, other_length) == 0) :
(CASE_N_COMPARE(str.data(), m_buffer + start_offset, other_length) == 0);
}
bool SmallStringBase::ends_with(const std::string& str, bool case_sensitive) const
{
const u32 other_length = static_cast<u32>(str.length());
if (other_length > m_length)
return false;
const u32 start_offset = m_length - other_length;
return (case_sensitive) ? (std::strncmp(str.data(), m_buffer + start_offset, other_length) == 0) :
(CASE_N_COMPARE(str.data(), m_buffer + start_offset, other_length) == 0);
}
void SmallStringBase::clear()
{
// in debug, zero whole string, in release, zero only the first character
#if _DEBUG
std::memset(m_buffer, 0, m_buffer_size);
#else
m_buffer[0] = '\0';
#endif
m_length = 0;
}
s32 SmallStringBase::find(char c, u32 offset) const
{
if (m_length == 0)
return -1;
pxAssert(offset <= m_length);
const char* at = std::strchr(m_buffer + offset, c);
return at ? static_cast<s32>(at - m_buffer) : -1;
}
s32 SmallStringBase::rfind(char c, u32 offset) const
{
if (m_length == 0)
return -1;
pxAssert(offset <= m_length);
const char* at = std::strrchr(m_buffer + offset, c);
return at ? static_cast<s32>(at - m_buffer) : -1;
}
s32 SmallStringBase::find(const char* str, u32 offset) const
{
if (m_length == 0)
return -1;
pxAssert(offset <= m_length);
const char* at = std::strstr(m_buffer + offset, str);
return at ? static_cast<s32>(at - m_buffer) : -1;
}
u32 SmallStringBase::count(char ch) const
{
const char* ptr = m_buffer;
const char* end = ptr + m_length;
u32 count = 0;
while (ptr != end)
count += static_cast<u32>(*(ptr++) == ch);
return count;
}
void SmallStringBase::resize(u32 new_size, char fill, bool shrink_if_smaller)
{
// if going larger, or we don't own the buffer, realloc
if (new_size >= m_buffer_size)
{
reserve(new_size);
if (m_length < new_size)
{
std::memset(m_buffer + m_length, fill, m_buffer_size - m_length - 1);
}
m_length = new_size;
}
else
{
// update length and terminator
#if _DEBUG
std::memset(m_buffer + new_size, 0, m_buffer_size - new_size);
#else
m_buffer[new_size] = 0;
#endif
m_length = new_size;
// shrink if requested
if (shrink_if_smaller)
shrink_to_fit();
}
}
void SmallStringBase::update_size()
{
m_length = static_cast<u32>(std::strlen(m_buffer));
}
std::string_view SmallStringBase::substr(s32 offset, s32 count) const
{
// calc real offset
u32 real_offset;
if (offset < 0)
real_offset = static_cast<u32>(std::max<s32>(0, static_cast<s32>(m_length + offset)));
else
real_offset = std::min((u32)offset, m_length);
// calc real count
u32 real_count;
if (count < 0)
{
real_count =
std::min(m_length - real_offset, static_cast<u32>(std::max<s32>(0, static_cast<s32>(m_length) + count)));
}
else
{
real_count = std::min(m_length - real_offset, static_cast<u32>(count));
}
return (real_count > 0) ? std::string_view(m_buffer + real_offset, real_count) : std::string_view();
}
void SmallStringBase::erase(s32 offset, s32 count)
{
// calc real offset
u32 real_offset;
if (offset < 0)
real_offset = static_cast<u32>(std::max<s32>(0, static_cast<s32>(m_length + offset)));
else
real_offset = std::min((u32)offset, m_length);
// calc real count
u32 real_count;
if (count < 0)
{
real_count =
std::min(m_length - real_offset, static_cast<u32>(std::max<s32>(0, static_cast<s32>(m_length) + count)));
}
else
{
real_count = std::min(m_length - real_offset, static_cast<u32>(count));
}
// Fastpath: offset == 0, count < 0, wipe whole string.
if (real_offset == 0 && real_count == m_length)
{
clear();
return;
}
// Fastpath: offset >= 0, count < 0, wipe everything after offset + count
if ((real_offset + real_count) == m_length)
{
m_length -= real_count;
#ifdef _DEBUG
std::memset(m_buffer + m_length, 0, m_buffer_size - m_length);
#else
m_buffer[m_length] = 0;
#endif
}
// Slowpath: offset >= 0, count < length
else
{
const u32 after_erase_block = m_length - real_offset - real_count;
pxAssert(after_erase_block > 0);
std::memmove(m_buffer + offset, m_buffer + real_offset + real_count, after_erase_block);
m_length = m_length - real_count;
#ifdef _DEBUG
std::memset(m_buffer + m_length, 0, m_buffer_size - m_length);
#else
m_buffer[m_length] = 0;
#endif
}
}

426
common/SmallString.h Normal file
View File

@@ -0,0 +1,426 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include "fmt/base.h"
#include <algorithm>
#include <cstdarg>
#include <cstring>
#include <iterator>
#include <limits>
#include <string>
#include <string_view>
//
// SmallString
// Lightweight string class which can be allocated on the stack, instead of with heap allocations.
//
class SmallStringBase
{
public:
using value_type = char;
SmallStringBase();
SmallStringBase(const char* str);
SmallStringBase(const char* str, u32 length);
SmallStringBase(const SmallStringBase& copy);
SmallStringBase(SmallStringBase&& move);
SmallStringBase(const std::string& str);
SmallStringBase(const std::string_view sv);
// Destructor. Child classes may not have any destructors, as this is not virtual.
~SmallStringBase();
// manual assignment
void assign(const char* str);
void assign(const char* str, u32 length);
void assign(const std::string& copy);
void assign(const std::string_view copy);
void assign(const SmallStringBase& copy);
void assign(SmallStringBase&& move);
// Ensures that we have space bytes free in the buffer.
void make_room_for(u32 space);
// clears the contents of the string
void clear();
// append a single character to this string
void append(char c);
// append a string to this string
void append(const char* appendText);
void append(const char* str, u32 length);
void append(const std::string& str);
void append(const std::string_view str);
void append(const SmallStringBase& str);
// append formatted string to this string
void append_sprintf(const char* format, ...) /*printflike(2, 3)*/;
void append_vsprintf(const char* format, va_list ap);
template <typename... T>
void append_format(fmt::format_string<T...> fmt, T&&... args);
// append hex string
void append_hex(const void* data, size_t len);
// append a single character to this string
void prepend(char c);
// append a string to this string
void prepend(const char* str);
void prepend(const char* str, u32 length);
void prepend(const std::string& str);
void prepend(const std::string_view str);
void prepend(const SmallStringBase& str);
// append formatted string to this string
void prepend_sprintf(const char* format, ...) /*printflike(2, 3)*/;
void prepend_vsprintf(const char* format, va_list ap);
template <typename... T>
void prepend_format(fmt::format_string<T...> fmt, T&&... args);
// insert a string at the specified offset
void insert(s32 offset, const char* str);
void insert(s32 offset, const char* str, u32 length);
void insert(s32 offset, const std::string& str);
void insert(s32 offset, const std::string_view str);
void insert(s32 offset, const SmallStringBase& str);
// set to formatted string
void sprintf(const char* format, ...) /*printflike(2, 3)*/;
void vsprintf(const char* format, va_list ap);
template <typename... T>
void format(fmt::format_string<T...> fmt, T&&... args);
void vformat(fmt::string_view fmt, fmt::format_args args);
// compare one string to another
bool equals(const char* str) const;
bool equals(const SmallStringBase& str) const;
bool equals(const std::string_view str) const;
bool equals(const std::string& str) const;
bool iequals(const char* str) const;
bool iequals(const SmallStringBase& str) const;
bool iequals(const std::string_view str) const;
bool iequals(const std::string& str) const;
// numerical compares
int compare(const char* str) const;
int compare(const SmallStringBase& str) const;
int compare(const std::string_view str) const;
int compare(const std::string& str) const;
int icompare(const char* str) const;
int icompare(const SmallStringBase& str) const;
int icompare(const std::string_view str) const;
int icompare(const std::string& str) const;
// starts with / ends with
bool starts_with(const char* str, bool case_sensitive = true) const;
bool starts_with(const SmallStringBase& str, bool case_sensitive = true) const;
bool starts_with(const std::string_view str, bool case_sensitive = true) const;
bool starts_with(const std::string& str, bool case_sensitive = true) const;
bool ends_with(const char* str, bool case_sensitive = true) const;
bool ends_with(const SmallStringBase& str, bool case_sensitive = true) const;
bool ends_with(const std::string_view str, bool case_sensitive = true) const;
bool ends_with(const std::string& str, bool case_sensitive = true) const;
// searches for a character inside a string
// rfind is the same except it starts at the end instead of the start
// returns -1 if it is not found, otherwise the offset in the string
s32 find(char c, u32 offset = 0) const;
s32 rfind(char c, u32 offset = 0) const;
// searches for a string inside a string
// rfind is the same except it starts at the end instead of the start
// returns -1 if it is not found, otherwise the offset in the string
s32 find(const char* str, u32 offset = 0) const;
// returns the number of instances of the specified character
u32 count(char ch) const;
// removes characters from string
void erase(s32 offset, s32 count = std::numeric_limits<s32>::max());
// alters the length of the string to be at least len bytes long
void reserve(u32 new_reserve);
// Cuts characters off the string to reduce it to len bytes long.
void resize(u32 new_size, char fill = ' ', bool shrink_if_smaller = false);
// updates the internal length counter when the string is externally modified
void update_size();
// shrink the string to the minimum size possible
void shrink_to_fit();
// gets the size of the string
__fi u32 length() const { return m_length; }
__fi bool empty() const { return (m_length == 0); }
// gets the maximum number of bytes we can write to the string, currently
__fi u32 buffer_size() const { return m_buffer_size; }
// gets a constant pointer to the C string
__fi const char* c_str() const { return m_buffer; }
// gets a writable char array, do not write more than reserve characters to it.
__fi char* data() { return m_buffer; }
// returns the end of the string (pointer is past the last character)
__fi const char* end_ptr() const { return m_buffer + m_length; }
// STL adapters
__fi void push_back(value_type val) { append(val); }
// returns a string view for this string
std::string_view view() const;
// returns a substring view for this string
std::string_view substr(s32 offset, s32 count) const;
// accessor operators
__fi operator const char*() const { return c_str(); }
__fi operator char*() { return data(); }
__fi operator std::string_view() const { return view(); }
// comparative operators
__fi bool operator==(const char* str) const { return equals(str); }
__fi bool operator==(const SmallStringBase& str) const { return equals(str); }
__fi bool operator==(const std::string_view str) const { return equals(str); }
__fi bool operator==(const std::string& str) const { return equals(str); }
__fi bool operator!=(const char* str) const { return !equals(str); }
__fi bool operator!=(const SmallStringBase& str) const { return !equals(str); }
__fi bool operator!=(const std::string_view str) const { return !equals(str); }
__fi bool operator!=(const std::string& str) const { return !equals(str); }
__fi bool operator<(const char* str) const { return (compare(str) < 0); }
__fi bool operator<(const SmallStringBase& str) const { return (compare(str) < 0); }
__fi bool operator<(const std::string_view str) const { return (compare(str) < 0); }
__fi bool operator<(const std::string& str) const { return (compare(str) < 0); }
__fi bool operator>(const char* str) const { return (compare(str) > 0); }
__fi bool operator>(const SmallStringBase& str) const { return (compare(str) > 0); }
__fi bool operator>(const std::string_view str) const { return (compare(str) > 0); }
__fi bool operator>(const std::string& str) const { return (compare(str) > 0); }
SmallStringBase& operator=(const SmallStringBase& copy);
SmallStringBase& operator=(const char* str);
SmallStringBase& operator=(const std::string& str);
SmallStringBase& operator=(const std::string_view str);
SmallStringBase& operator=(SmallStringBase&& move);
protected:
// Pointer to memory where the string is located
char* m_buffer = nullptr;
// Length of the string located in pBuffer (in characters)
u32 m_length = 0;
// Size of the buffer pointed to by pBuffer
u32 m_buffer_size = 0;
// True if the string is dynamically allocated on the heap.
bool m_on_heap = false;
};
// stack-allocated string
template <u32 L>
class SmallStackString : public SmallStringBase
{
public:
__fi SmallStackString() { init(); }
__fi SmallStackString(const char* str)
{
init();
assign(str);
}
__fi SmallStackString(const char* str, u32 length)
{
init();
assign(str, length);
}
__fi SmallStackString(const SmallStringBase& copy)
{
init();
assign(copy);
}
__fi SmallStackString(SmallStringBase&& move)
{
init();
assign(move);
}
__fi SmallStackString(const SmallStackString& copy)
{
init();
assign(copy);
}
__fi SmallStackString(SmallStackString&& move)
{
init();
assign(move);
}
__fi SmallStackString(const std::string_view sv)
{
init();
assign(sv);
}
__fi SmallStackString& operator=(const SmallStringBase& copy)
{
assign(copy);
return *this;
}
__fi SmallStackString& operator=(SmallStringBase&& move)
{
assign(move);
return *this;
}
__fi SmallStackString& operator=(const SmallStackString& copy)
{
assign(copy);
return *this;
}
__fi SmallStackString& operator=(SmallStackString&& move)
{
assign(move);
return *this;
}
__fi SmallStackString& operator=(const std::string_view sv)
{
assign(sv);
return *this;
}
__fi SmallStackString& operator=(const char* str)
{
assign(str);
return *this;
}
// Override the fromstring method
static SmallStackString from_sprintf(const char* format, ...) /*printflike(1, 2)*/;
template <typename... T>
static SmallStackString from_format(fmt::format_string<T...> fmt, T&&... args);
static SmallStackString from_vformat(fmt::string_view fmt, fmt::format_args args);
private:
char m_stack_buffer[L + 1];
__fi void init()
{
m_buffer = m_stack_buffer;
m_buffer_size = L + 1;
#ifdef _DEBUG
std::memset(m_stack_buffer, 0, sizeof(m_stack_buffer));
#else
m_stack_buffer[0] = '\0';
#endif
}
};
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4459) // warning C4459: declaration of 'uint' hides global declaration
#endif
template <u32 L>
SmallStackString<L> SmallStackString<L>::from_sprintf(const char* format, ...)
{
std::va_list ap;
va_start(ap, format);
SmallStackString ret;
ret.vsprintf(format, ap);
va_end(ap);
return ret;
}
template <u32 L>
template <typename... T>
__fi SmallStackString<L> SmallStackString<L>::from_format(fmt::format_string<T...> fmt, T&&... args)
{
SmallStackString<L> ret;
fmt::vformat_to(std::back_inserter(ret), fmt, fmt::make_format_args(args...));
return ret;
}
template <u32 L>
__fi SmallStackString<L> SmallStackString<L>::from_vformat(fmt::string_view fmt, fmt::format_args args)
{
SmallStackString<L> ret;
fmt::vformat_to(std::back_inserter(ret), fmt, args);
return ret;
}
// stack string types
using TinyString = SmallStackString<64>;
using SmallString = SmallStackString<256>;
template <typename... T>
__fi void SmallStringBase::append_format(fmt::format_string<T...> fmt, T&&... args)
{
fmt::vformat_to(std::back_inserter(*this), fmt, fmt::make_format_args(args...));
}
template <typename... T>
__fi void SmallStringBase::prepend_format(fmt::format_string<T...> fmt, T&&... args)
{
TinyString str;
fmt::vformat_to(std::back_inserter(str), fmt, fmt::make_format_args(args...));
prepend(str);
}
template <typename... T>
__fi void SmallStringBase::format(fmt::format_string<T...> fmt, T&&... args)
{
clear();
fmt::vformat_to(std::back_inserter(*this), fmt, fmt::make_format_args(args...));
}
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#define MAKE_FORMATTER(type) \
template <> \
struct fmt::formatter<type> \
{ \
template <typename ParseContext> \
constexpr auto parse(ParseContext& ctx) \
{ \
return ctx.begin(); \
} \
\
template <typename FormatContext> \
auto format(const type& str, FormatContext& ctx) const \
{ \
return fmt::format_to(ctx.out(), "{}", str.view()); \
} \
};
MAKE_FORMATTER(TinyString);
MAKE_FORMATTER(SmallString);
#undef MAKE_FORMATTER

1229
common/StackWalker.cpp Normal file

File diff suppressed because it is too large Load Diff

181
common/StackWalker.h Normal file
View File

@@ -0,0 +1,181 @@
/**********************************************************************
*
* StackWalker.h
*
*
*
* LICENSE (http://www.opensource.org/licenses/bsd-license.php)
*
* Copyright (c) 2005-2009, Jochen Kalmbach
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* Neither the name of Jochen Kalmbach nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* **********************************************************************/
#pragma once
#include "RedtapeWindows.h"
class StackWalkerInternal; // forward
class StackWalker
{
public:
typedef enum StackWalkOptions
{
// No addition info will be retrieved
// (only the address is available)
RetrieveNone = 0,
// Try to get the symbol-name
RetrieveSymbol = 1,
// Try to get the line for this symbol
RetrieveLine = 2,
// Try to retrieve the module-infos
RetrieveModuleInfo = 4,
// Also retrieve the version for the DLL/EXE
RetrieveFileVersion = 8,
// Contains all the above
RetrieveVerbose = 0xF,
// Generate a "good" symbol-search-path
SymBuildPath = 0x10,
// Also use the public Microsoft-Symbol-Server
SymUseSymSrv = 0x20,
// Contains all the above "Sym"-options
SymAll = 0x30,
// Contains all options (default)
OptionsAll = 0x3F
} StackWalkOptions;
StackWalker(int options = OptionsAll, // 'int' is by design, to combine the enum-flags
LPCSTR szSymPath = NULL,
DWORD dwProcessId = GetCurrentProcessId(),
HANDLE hProcess = GetCurrentProcess());
StackWalker(DWORD dwProcessId, HANDLE hProcess);
StackWalker(const StackWalker&) = delete;
virtual ~StackWalker();
static HMODULE LoadDbgHelpLibrary();
typedef BOOL(__stdcall* PReadProcessMemoryRoutine)(
HANDLE hProcess,
DWORD64 qwBaseAddress,
PVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesRead,
LPVOID pUserData // optional data, which was passed in "ShowCallstack"
);
BOOL LoadModules();
BOOL ShowCallstack(
HANDLE hThread = GetCurrentThread(),
const CONTEXT* context = NULL,
PReadProcessMemoryRoutine readMemoryFunction = NULL,
LPVOID pUserData = NULL // optional to identify some data in the 'readMemoryFunction'-callback
);
BOOL ShowObject(LPVOID pObject);
#if _MSC_VER >= 1300
// due to some reasons, the "STACKWALK_MAX_NAMELEN" must be declared as "public"
// in older compilers in order to use it... starting with VC7 we can declare it as "protected"
protected:
#endif
enum
{
STACKWALK_MAX_NAMELEN = 1024
}; // max name length for found symbols
protected:
// Entry for each Callstack-Entry
typedef struct CallstackEntry
{
DWORD64 offset; // if 0, we have no valid entry
CHAR name[STACKWALK_MAX_NAMELEN];
CHAR undName[STACKWALK_MAX_NAMELEN];
CHAR undFullName[STACKWALK_MAX_NAMELEN];
DWORD64 offsetFromSmybol;
DWORD offsetFromLine;
DWORD lineNumber;
CHAR lineFileName[STACKWALK_MAX_NAMELEN];
DWORD symType;
LPCSTR symTypeString;
CHAR moduleName[STACKWALK_MAX_NAMELEN];
DWORD64 baseOfImage;
CHAR loadedImageName[STACKWALK_MAX_NAMELEN];
} CallstackEntry;
typedef enum CallstackEntryType
{
firstEntry,
nextEntry,
lastEntry
} CallstackEntryType;
virtual void OnSymInit(LPCSTR szSearchPath, DWORD symOptions, LPCSTR szUserName);
virtual void OnLoadModule(LPCSTR img,
LPCSTR mod,
DWORD64 baseAddr,
DWORD size,
DWORD result,
LPCSTR symType,
LPCSTR pdbName,
ULONGLONG fileVersion);
virtual void OnCallstackEntry(CallstackEntryType eType, CallstackEntry& entry);
virtual void OnDbgHelpErr(LPCSTR szFuncName, DWORD gle, DWORD64 addr);
virtual void OnOutput(LPCSTR szText);
StackWalkerInternal* m_sw;
HANDLE m_hProcess;
DWORD m_dwProcessId;
BOOL m_modulesLoaded;
LPSTR m_szSymPath;
int m_options;
int m_MaxRecursionCount;
static BOOL __stdcall myReadProcMem(HANDLE hProcess,
DWORD64 qwBaseAddress,
PVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesRead);
friend StackWalkerInternal;
}; // class StackWalker
// The following is defined for x86 (XP and higher), x64 and IA64:
#define GET_CURRENT_CONTEXT_STACKWALKER_CODEPLEX(c, contextFlags) \
do \
{ \
memset(&c, 0, sizeof(CONTEXT)); \
c.ContextFlags = contextFlags; \
RtlCaptureContext(&c); \
} while (0);

544
common/StringUtil.cpp Normal file
View File

@@ -0,0 +1,544 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Assertions.h"
#include "StringUtil.h"
#include <cctype>
#include <codecvt>
#include <cstdio>
#include <sstream>
#include <algorithm>
#include "fmt/format.h"
#ifdef _WIN32
#include "RedtapeWindows.h"
#endif
namespace StringUtil
{
std::string StdStringFromFormat(const char* format, ...)
{
std::va_list ap;
va_start(ap, format);
std::string ret = StdStringFromFormatV(format, ap);
va_end(ap);
return ret;
}
std::string StdStringFromFormatV(const char* format, std::va_list ap)
{
std::va_list ap_copy;
va_copy(ap_copy, ap);
#ifdef _WIN32
int len = _vscprintf(format, ap_copy);
#else
int len = std::vsnprintf(nullptr, 0, format, ap_copy);
#endif
va_end(ap_copy);
std::string ret;
// If an encoding error occurs, len is -1. Which we definitely don't want to resize to.
if (len > 0)
{
ret.resize(len);
std::vsnprintf(ret.data(), ret.size() + 1, format, ap);
}
return ret;
}
bool WildcardMatch(const char* subject, const char* mask, bool case_sensitive /*= true*/)
{
if (case_sensitive)
{
const char* cp = nullptr;
const char* mp = nullptr;
while ((*subject) && (*mask != '*'))
{
if ((*mask != '?') && (std::tolower(*mask) != std::tolower(*subject)))
return false;
mask++;
subject++;
}
while (*subject)
{
if (*mask == '*')
{
if (*++mask == 0)
return true;
mp = mask;
cp = subject + 1;
}
else
{
if ((*mask == '?') || (std::tolower(*mask) == std::tolower(*subject)))
{
mask++;
subject++;
}
else
{
mask = mp;
subject = cp++;
}
}
}
while (*mask == '*')
{
mask++;
}
return *mask == 0;
}
else
{
const char* cp = nullptr;
const char* mp = nullptr;
while ((*subject) && (*mask != '*'))
{
if ((*mask != *subject) && (*mask != '?'))
return false;
mask++;
subject++;
}
while (*subject)
{
if (*mask == '*')
{
if (*++mask == 0)
return true;
mp = mask;
cp = subject + 1;
}
else
{
if ((*mask == *subject) || (*mask == '?'))
{
mask++;
subject++;
}
else
{
mask = mp;
subject = cp++;
}
}
}
while (*mask == '*')
{
mask++;
}
return *mask == 0;
}
}
std::size_t Strlcpy(char* dst, const char* src, std::size_t size)
{
std::size_t len = std::strlen(src);
if (len < size)
{
std::memcpy(dst, src, len + 1);
}
else
{
std::memcpy(dst, src, size - 1);
dst[size - 1] = '\0';
}
return len;
}
std::size_t Strlcpy(char* dst, const std::string_view src, std::size_t size)
{
std::size_t len = src.length();
if (len < size)
{
std::memcpy(dst, src.data(), len);
dst[len] = '\0';
}
else
{
std::memcpy(dst, src.data(), size - 1);
dst[size - 1] = '\0';
}
return len;
}
std::optional<std::vector<u8>> DecodeHex(const std::string_view in)
{
std::vector<u8> data;
data.reserve(in.size() / 2);
for (size_t i = 0; i < in.size() / 2; i++)
{
std::optional<u8> byte = StringUtil::FromChars<u8>(in.substr(i * 2, 2), 16);
if (byte.has_value())
data.push_back(*byte);
else
return std::nullopt;
}
return {data};
}
std::string EncodeHex(const u8* data, int length)
{
std::stringstream ss;
for (int i = 0; i < length; i++)
ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(data[i]);
return ss.str();
}
std::string toLower(const std::string_view input)
{
std::string newStr;
std::transform(input.begin(), input.end(), std::back_inserter(newStr),
[](unsigned char c) { return std::tolower(c); });
return newStr;
}
std::string toUpper(const std::string_view input)
{
std::string newStr;
std::transform(input.begin(), input.end(), std::back_inserter(newStr),
[](unsigned char c) { return std::toupper(c); });
return newStr;
}
bool compareNoCase(const std::string_view str1, const std::string_view str2)
{
if (str1.length() != str2.length())
{
return false;
}
return Strncasecmp(str1.data(), str2.data(), str1.length()) == 0;
}
std::vector<std::string> splitOnNewLine(const std::string& str)
{
std::vector<std::string> lines;
std::istringstream stream(str);
std::string line;
while (std::getline(stream, line))
{
lines.push_back(line);
}
return lines;
}
std::string_view StripWhitespace(const std::string_view str)
{
std::string_view::size_type start = 0;
while (start < str.size() && std::isspace(str[start]))
start++;
if (start == str.size())
return {};
std::string_view::size_type end = str.size() - 1;
while (end > start && std::isspace(str[end]))
end--;
return str.substr(start, end - start + 1);
}
void StripWhitespace(std::string* str)
{
{
const char* cstr = str->c_str();
std::string_view::size_type start = 0;
while (start < str->size() && std::isspace(cstr[start]))
start++;
if (start != 0)
str->erase(0, start);
}
{
const char* cstr = str->c_str();
std::string_view::size_type start = str->size();
while (start > 0 && std::isspace(cstr[start - 1]))
start--;
if (start != str->size())
str->erase(start);
}
}
std::vector<std::string_view> SplitString(const std::string_view str, char delimiter, bool skip_empty /*= true*/)
{
std::vector<std::string_view> res;
std::string_view::size_type last_pos = 0;
std::string_view::size_type pos;
while (last_pos < str.size() && (pos = str.find(delimiter, last_pos)) != std::string_view::npos)
{
std::string_view part(StripWhitespace(str.substr(last_pos, pos - last_pos)));
if (!skip_empty || !part.empty())
res.push_back(std::move(part));
last_pos = pos + 1;
}
if (last_pos < str.size())
{
std::string_view part(StripWhitespace(str.substr(last_pos)));
if (!skip_empty || !part.empty())
res.push_back(std::move(part));
}
return res;
}
std::string ReplaceAll(const std::string_view subject, const std::string_view search, const std::string_view replacement)
{
std::string ret(subject);
ReplaceAll(&ret, search, replacement);
return ret;
}
void ReplaceAll(std::string* subject, const std::string_view search, const std::string_view replacement)
{
if (!subject->empty())
{
std::string::size_type start_pos = 0;
while ((start_pos = subject->find(search, start_pos)) != std::string::npos)
{
subject->replace(start_pos, search.length(), replacement);
start_pos += replacement.length();
}
}
}
bool ParseAssignmentString(const std::string_view str, std::string_view* key, std::string_view* value)
{
const std::string_view::size_type pos = str.find('=');
if (pos == std::string_view::npos)
{
*key = std::string_view();
*value = std::string_view();
return false;
}
*key = StripWhitespace(str.substr(0, pos));
if (pos != (str.size() - 1))
*value = StripWhitespace(str.substr(pos + 1));
else
*value = std::string_view();
return true;
}
void AppendUTF16CharacterToUTF8(std::string& s, u16 ch)
{
if (ch & 0xf800)
{
s.push_back(static_cast<char>(static_cast<u8>(0xe0 | static_cast<u8>(ch >> 12))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>(((ch >> 6) & 0x3f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>((ch & 0x3f)))));
}
else if (ch & 0xff80)
{
s.push_back(static_cast<char>(static_cast<u8>(0xc0 | static_cast<u8>((ch >> 6)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>((ch & 0x3f)))));
}
else
{
s.push_back(static_cast<char>(static_cast<u8>(ch)));
}
}
void EncodeAndAppendUTF8(std::string& s, char32_t ch)
{
if (ch <= 0x7F)
{
s.push_back(static_cast<char>(static_cast<u8>(ch)));
}
else if (ch <= 0x07FF)
{
s.push_back(static_cast<char>(static_cast<u8>(0xc0 | static_cast<u8>((ch >> 6) & 0x1f))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>((ch & 0x3f)))));
}
else if (ch <= 0xFFFF)
{
s.push_back(static_cast<char>(static_cast<u8>(0xe0 | static_cast<u8>(((ch >> 12) & 0x0f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>(((ch >> 6) & 0x3f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>((ch & 0x3f)))));
}
else if (ch <= 0x10FFFF)
{
s.push_back(static_cast<char>(static_cast<u8>(0xf0 | static_cast<u8>(((ch >> 18) & 0x07)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>(((ch >> 12) & 0x3f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>(((ch >> 6) & 0x3f)))));
s.push_back(static_cast<char>(static_cast<u8>(0x80 | static_cast<u8>((ch & 0x3f)))));
}
else
{
s.push_back(static_cast<char>(0xefu));
s.push_back(static_cast<char>(0xbfu));
s.push_back(static_cast<char>(0xbdu));
}
}
size_t DecodeUTF8(const void* bytes, size_t length, char32_t* ch)
{
const u8* s = reinterpret_cast<const u8*>(bytes);
if (s[0] < 0x80)
{
*ch = s[0];
return 1;
}
else if ((s[0] & 0xe0) == 0xc0)
{
if (length < 2)
goto invalid;
*ch = static_cast<char32_t>((static_cast<u32>(s[0] & 0x1f) << 6) | (static_cast<u32>(s[1] & 0x3f) << 0));
return 2;
}
else if ((s[0] & 0xf0) == 0xe0)
{
if (length < 3)
goto invalid;
*ch = static_cast<char32_t>((static_cast<u32>(s[0] & 0x0f) << 12) | (static_cast<u32>(s[1] & 0x3f) << 6) |
(static_cast<u32>(s[2] & 0x3f) << 0));
return 3;
}
else if ((s[0] & 0xf8) == 0xf0 && (s[0] <= 0xf4))
{
if (length < 4)
goto invalid;
*ch = static_cast<char32_t>((static_cast<u32>(s[0] & 0x07) << 18) | (static_cast<u32>(s[1] & 0x3f) << 12) |
(static_cast<u32>(s[2] & 0x3f) << 6) | (static_cast<u32>(s[3] & 0x3f) << 0));
return 4;
}
invalid:
*ch = 0xFFFFFFFFu;
return 1;
}
size_t DecodeUTF8(const std::string_view str, size_t offset, char32_t* ch)
{
return DecodeUTF8(str.data() + offset, str.length() - offset, ch);
}
size_t DecodeUTF8(const std::string& str, size_t offset, char32_t* ch)
{
return DecodeUTF8(str.data() + offset, str.length() - offset, ch);
}
std::string Ellipsise(const std::string_view str, u32 max_length, const char* ellipsis /*= "..."*/)
{
std::string ret;
ret.reserve(max_length);
const u32 str_length = static_cast<u32>(str.length());
const u32 ellipsis_len = static_cast<u32>(std::strlen(ellipsis));
pxAssert(ellipsis_len > 0 && ellipsis_len <= max_length);
if (str_length > max_length)
{
const u32 copy_size = std::min(str_length, max_length - ellipsis_len);
if (copy_size > 0)
ret.append(str.data(), copy_size);
if (copy_size != str_length)
ret.append(ellipsis);
}
else
{
ret.append(str);
}
return ret;
}
void EllipsiseInPlace(std::string& str, u32 max_length, const char* ellipsis /*= "..."*/)
{
const u32 str_length = static_cast<u32>(str.length());
const u32 ellipsis_len = static_cast<u32>(std::strlen(ellipsis));
pxAssert(ellipsis_len > 0 && ellipsis_len <= max_length);
if (str_length > max_length)
{
const u32 keep_size = std::min(static_cast<u32>(str.length()), max_length - ellipsis_len);
if (keep_size != str_length)
str.erase(keep_size);
str.append(ellipsis);
}
}
#ifdef _WIN32
std::wstring UTF8StringToWideString(const std::string_view str)
{
std::wstring ret;
if (!UTF8StringToWideString(ret, str))
return {};
return ret;
}
bool UTF8StringToWideString(std::wstring& dest, const std::string_view str)
{
int wlen = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast<int>(str.length()), nullptr, 0);
if (wlen < 0)
return false;
dest.resize(wlen);
if (wlen > 0 && MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast<int>(str.length()), dest.data(), wlen) < 0)
return false;
return true;
}
std::string WideStringToUTF8String(const std::wstring_view& str)
{
std::string ret;
if (!WideStringToUTF8String(ret, str))
ret.clear();
return ret;
}
bool WideStringToUTF8String(std::string& dest, const std::wstring_view& str)
{
int mblen = WideCharToMultiByte(CP_UTF8, 0, str.data(), static_cast<int>(str.length()), nullptr, 0, nullptr, nullptr);
if (mblen < 0)
return false;
dest.resize(mblen);
if (mblen > 0 && WideCharToMultiByte(CP_UTF8, 0, str.data(), static_cast<int>(str.length()), dest.data(), mblen,
nullptr, nullptr) < 0)
{
return false;
}
return true;
}
#endif
std::string U128ToString(const u128& u)
{
return fmt::format("0x{:08X}.{:08X}.{:08X}.{:08X}", u._u32[0], u._u32[1], u._u32[2], u._u32[3]);
}
std::string& AppendU128ToString(const u128& u, std::string& s)
{
fmt::format_to(std::back_inserter(s), "0x{:08X}.{:08X}.{:08X}.{:08X}", u._u32[0], u._u32[1], u._u32[2], u._u32[3]);
return s;
}
} // namespace StringUtil

345
common/StringUtil.h Normal file
View File

@@ -0,0 +1,345 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Types.h"
#include <algorithm>
#include <charconv>
#include <cstdarg>
#include <cstddef>
#include <cstring>
#include <iomanip>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
// Work around us defining _M_ARM64 but fast_float thinking that it means MSVC.
#if defined(_M_ARM64) && !defined(_WIN32)
#define HAD_M_ARM64 _M_ARM64
#undef _M_ARM64
#endif
#include "fast_float/fast_float.h"
#if defined(HAD_M_ARM64) && !defined(_WIN32)
#define _M_ARM64 HAD_M_ARM64
#undef HAD_M_ARM64
#endif
// Older versions of libstdc++ are missing support for from_chars() with floats, and was only recently
// merged in libc++. So, just fall back to stringstream (yuck!) on everywhere except MSVC.
#if !defined(_MSC_VER)
#include <locale>
#include <sstream>
#ifdef __APPLE__
#include <Availability.h>
#endif
#endif
namespace StringUtil
{
/// Constructs a std::string from a format string.
#ifdef __GNUC__
std::string StdStringFromFormat(const char* format, ...) __attribute__((format(printf, 1, 2)));
#else
std::string StdStringFromFormat(const char* format, ...);
#endif
std::string StdStringFromFormatV(const char* format, std::va_list ap);
/// Checks if a wildcard matches a search string.
bool WildcardMatch(const char* subject, const char* mask, bool case_sensitive = true);
/// Safe version of strlcpy.
std::size_t Strlcpy(char* dst, const char* src, std::size_t size);
/// Strlcpy from string_view.
std::size_t Strlcpy(char* dst, const std::string_view src, std::size_t size);
/// Platform-independent strcasecmp
static inline int Strcasecmp(const char* s1, const char* s2)
{
#ifdef _MSC_VER
return _stricmp(s1, s2);
#else
return strcasecmp(s1, s2);
#endif
}
/// Platform-independent strcasecmp
static inline int Strncasecmp(const char* s1, const char* s2, std::size_t n)
{
#ifdef _MSC_VER
return _strnicmp(s1, s2, n);
#else
return strncasecmp(s1, s2, n);
#endif
}
/// Wrapper around std::from_chars
template <typename T, std::enable_if_t<std::is_integral<T>::value, bool> = true>
inline std::optional<T> FromChars(const std::string_view str, int base = 10)
{
T value;
const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.length(), value, base);
if (result.ec != std::errc())
return std::nullopt;
return value;
}
template <typename T, std::enable_if_t<std::is_integral<T>::value, bool> = true>
inline std::optional<T> FromChars(const std::string_view str, int base, std::string_view* endptr)
{
T value;
const char* ptr = str.data();
const char* end = ptr + str.length();
const std::from_chars_result result = std::from_chars(ptr, end, value, base);
if (result.ec != std::errc())
return std::nullopt;
if (endptr)
*endptr = (result.ptr < end) ? std::string_view(result.ptr, end - result.ptr) : std::string_view();
return value;
}
template <typename T, std::enable_if_t<std::is_floating_point<T>::value, bool> = true>
inline std::optional<T> FromChars(const std::string_view str)
{
T value;
const fast_float::from_chars_result result = fast_float::from_chars(str.data(), str.data() + str.length(), value);
if (result.ec != std::errc())
return std::nullopt;
return value;
}
template <typename T, std::enable_if_t<std::is_floating_point<T>::value, bool> = true>
inline std::optional<T> FromChars(const std::string_view str, std::string_view* endptr)
{
T value;
const char* ptr = str.data();
const char* end = ptr + str.length();
const fast_float::from_chars_result result = fast_float::from_chars(ptr, end, value);
if (result.ec != std::errc())
return std::nullopt;
if (endptr)
*endptr = (result.ptr < end) ? std::string_view(result.ptr, end - result.ptr) : std::string_view();
return value;
}
/// Wrapper around std::to_chars
template <typename T, std::enable_if_t<std::is_integral<T>::value, bool> = true>
inline std::string ToChars(T value, int base = 10)
{
// to_chars() requires macOS 10.15+.
#if !defined(__APPLE__) || MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_15
constexpr size_t MAX_SIZE = 32;
char buf[MAX_SIZE];
std::string ret;
const std::to_chars_result result = std::to_chars(buf, buf + MAX_SIZE, value, base);
if (result.ec == std::errc())
ret.append(buf, result.ptr - buf);
return ret;
#else
std::ostringstream ss;
ss.imbue(std::locale::classic());
ss << std::setbase(base) << value;
return ss.str();
#endif
}
template <typename T, std::enable_if_t<std::is_floating_point<T>::value, bool> = true>
inline std::string ToChars(T value)
{
// No to_chars() in older versions of libstdc++/libc++.
#ifdef _MSC_VER
constexpr size_t MAX_SIZE = 64;
char buf[MAX_SIZE];
std::string ret;
const std::to_chars_result result = std::to_chars(buf, buf + MAX_SIZE, value);
if (result.ec == std::errc())
ret.append(buf, result.ptr - buf);
return ret;
#else
std::ostringstream ss;
ss.imbue(std::locale::classic());
ss << value;
return ss.str();
#endif
}
/// Explicit override for booleans
template <>
inline std::optional<bool> FromChars(const std::string_view str, int base)
{
if (Strncasecmp("true", str.data(), str.length()) == 0 || Strncasecmp("yes", str.data(), str.length()) == 0 ||
Strncasecmp("on", str.data(), str.length()) == 0 || Strncasecmp("1", str.data(), str.length()) == 0 ||
Strncasecmp("enabled", str.data(), str.length()) == 0 || Strncasecmp("1", str.data(), str.length()) == 0)
{
return true;
}
if (Strncasecmp("false", str.data(), str.length()) == 0 || Strncasecmp("no", str.data(), str.length()) == 0 ||
Strncasecmp("off", str.data(), str.length()) == 0 || Strncasecmp("0", str.data(), str.length()) == 0 ||
Strncasecmp("disabled", str.data(), str.length()) == 0 || Strncasecmp("0", str.data(), str.length()) == 0)
{
return false;
}
return std::nullopt;
}
template <>
inline std::string ToChars(bool value, int base)
{
return std::string(value ? "true" : "false");
}
/// Encode/decode hexadecimal byte buffers
std::optional<std::vector<u8>> DecodeHex(const std::string_view str);
std::string EncodeHex(const u8* data, int length);
/// StartsWith/EndsWith variants which aren't case sensitive.
static inline bool StartsWithNoCase(const std::string_view str, const std::string_view prefix)
{
return (!str.empty() && Strncasecmp(str.data(), prefix.data(), prefix.length()) == 0);
}
static inline bool EndsWithNoCase(const std::string_view str, const std::string_view suffix)
{
const std::size_t suffix_length = suffix.length();
return (str.length() >= suffix_length && Strncasecmp(str.data() + (str.length() - suffix_length), suffix.data(), suffix_length) == 0);
}
/// Strip whitespace from the start/end of the string.
std::string_view StripWhitespace(const std::string_view str);
void StripWhitespace(std::string* str);
/// Splits a string based on a single character delimiter.
std::vector<std::string_view> SplitString(const std::string_view str, char delimiter, bool skip_empty = true);
/// Joins a string together using the specified delimiter.
template <typename T>
static inline std::string JoinString(const T& start, const T& end, char delimiter)
{
std::string ret;
for (auto it = start; it != end; ++it)
{
if (it != start)
ret += delimiter;
ret.append(*it);
}
return ret;
}
template <typename T>
static inline std::string JoinString(const T& start, const T& end, const std::string_view delimiter)
{
std::string ret;
for (auto it = start; it != end; ++it)
{
if (it != start)
ret.append(delimiter);
ret.append(*it);
}
return ret;
}
/// Replaces all instances of search in subject with replacement.
std::string ReplaceAll(const std::string_view subject, const std::string_view search, const std::string_view replacement);
void ReplaceAll(std::string* subject, const std::string_view search, const std::string_view replacement);
/// Parses an assignment string (Key = Value) into its two components.
bool ParseAssignmentString(const std::string_view str, std::string_view* key, std::string_view* value);
/// Appends a UTF-16/UTF-32 codepoint to a UTF-8 string.
void AppendUTF16CharacterToUTF8(std::string& s, u16 ch);
/// Appends a UTF-16/UTF-32 codepoint to a UTF-8 string.
void EncodeAndAppendUTF8(std::string& s, char32_t ch);
/// Decodes UTF-8 to a single codepoint, updating the position parameter.
/// Returns the number of bytes the codepoint took in the original string.
size_t DecodeUTF8(const void* bytes, size_t length, char32_t* ch);
size_t DecodeUTF8(const std::string_view str, size_t offset, char32_t* ch);
size_t DecodeUTF8(const std::string& str, size_t offset, char32_t* ch);
// Replaces the end of a string with ellipsis if it exceeds the specified length.
std::string Ellipsise(const std::string_view str, u32 max_length, const char* ellipsis = "...");
void EllipsiseInPlace(std::string& str, u32 max_length, const char* ellipsis = "...");
/// Strided memcpy/memcmp.
static inline void StrideMemCpy(void* dst, std::size_t dst_stride, const void* src, std::size_t src_stride,
std::size_t copy_size, std::size_t count)
{
if (src_stride == dst_stride && src_stride == copy_size)
{
std::memcpy(dst, src, src_stride * count);
return;
}
const u8* src_ptr = static_cast<const u8*>(src);
u8* dst_ptr = static_cast<u8*>(dst);
for (std::size_t i = 0; i < count; i++)
{
std::memcpy(dst_ptr, src_ptr, copy_size);
src_ptr += src_stride;
dst_ptr += dst_stride;
}
}
static inline int StrideMemCmp(const void* p1, std::size_t p1_stride, const void* p2, std::size_t p2_stride,
std::size_t copy_size, std::size_t count)
{
if (p1_stride == p2_stride && p1_stride == copy_size)
return std::memcmp(p1, p2, p1_stride * count);
const u8* p1_ptr = static_cast<const u8*>(p1);
const u8* p2_ptr = static_cast<const u8*>(p2);
for (std::size_t i = 0; i < count; i++)
{
int result = std::memcmp(p1_ptr, p2_ptr, copy_size);
if (result != 0)
return result;
p2_ptr += p2_stride;
p1_ptr += p1_stride;
}
return 0;
}
std::string toLower(const std::string_view str);
std::string toUpper(const std::string_view str);
bool compareNoCase(const std::string_view str1, const std::string_view str2);
std::vector<std::string> splitOnNewLine(const std::string& str);
#ifdef _WIN32
/// Converts the specified UTF-8 string to a wide string.
std::wstring UTF8StringToWideString(const std::string_view str);
bool UTF8StringToWideString(std::wstring& dest, const std::string_view str);
/// Converts the specified wide string to a UTF-8 string.
std::string WideStringToUTF8String(const std::wstring_view& str);
bool WideStringToUTF8String(std::string& dest, const std::wstring_view& str);
#endif
/// Converts unsigned 128-bit data to string.
std::string U128ToString(const u128& u);
std::string& AppendU128ToString(const u128& u, std::string& s);
template <typename ContainerType>
static inline bool ContainsSubString(const ContainerType& haystack, const std::string_view needle)
{
using ValueType = typename ContainerType::value_type;
if (needle.empty())
return std::empty(haystack);
return std::search(std::begin(haystack), std::end(haystack), reinterpret_cast<const ValueType*>(needle.data()),
reinterpret_cast<const ValueType*>(needle.data() + needle.length())) != std::end(haystack);
}
} // namespace StringUtil

1151
common/TextureDecompress.cpp Normal file

File diff suppressed because it is too large Load Diff

198
common/TextureDecompress.h Normal file
View File

@@ -0,0 +1,198 @@
// See TextureDecompress.cpp for license info.
#pragma once
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable:4201) // nonstandard extension used: nameless struct/union
#endif
#include <stdlib.h>
#include <stdint.h>
#include <algorithm>
#include <math.h>
#include <assert.h>
enum BC4Mode
{
BC4_UNORM = 0,
BC4_SNORM = 1
};
enum BC5Mode
{
BC5_UNORM = 0,
BC5_SNORM = 1
};
void DecompressBlockBC1(uint32_t x, uint32_t y, uint32_t stride,
const uint8_t* blockStorage, unsigned char* image);
void DecompressBlockBC2(uint32_t x, uint32_t y, uint32_t stride,
const uint8_t* blockStorage, unsigned char* image);
void DecompressBlockBC3(uint32_t x, uint32_t y, uint32_t stride,
const uint8_t* blockStorage, unsigned char* image);
void DecompressBlockBC4(uint32_t x, uint32_t y, uint32_t stride,
enum BC4Mode mode, const uint8_t* blockStorage, unsigned char* image);
void DecompressBlockBC5(uint32_t x, uint32_t y, uint32_t stride,
enum BC5Mode mode, const uint8_t* blockStorage, unsigned char* image);
namespace bc7decomp
{
enum eNoClamp { cNoClamp };
template <typename S> inline S clamp(S value, S low, S high) { return (value < low) ? low : ((value > high) ? high : value); }
class color_rgba
{
public:
union
{
uint8_t m_comps[4];
struct
{
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
};
};
inline color_rgba() = default;
inline color_rgba(int y)
{
set(y);
}
inline color_rgba(int y, int na)
{
set(y, na);
}
inline color_rgba(int sr, int sg, int sb, int sa)
{
set(sr, sg, sb, sa);
}
inline color_rgba(eNoClamp, int sr, int sg, int sb, int sa)
{
set_noclamp_rgba((uint8_t)sr, (uint8_t)sg, (uint8_t)sb, (uint8_t)sa);
}
inline color_rgba& set_noclamp_y(int y)
{
m_comps[0] = (uint8_t)y;
m_comps[1] = (uint8_t)y;
m_comps[2] = (uint8_t)y;
m_comps[3] = (uint8_t)255;
return *this;
}
inline color_rgba &set_noclamp_rgba(int sr, int sg, int sb, int sa)
{
m_comps[0] = (uint8_t)sr;
m_comps[1] = (uint8_t)sg;
m_comps[2] = (uint8_t)sb;
m_comps[3] = (uint8_t)sa;
return *this;
}
inline color_rgba &set(int y)
{
m_comps[0] = static_cast<uint8_t>(clamp<int>(y, 0, 255));
m_comps[1] = m_comps[0];
m_comps[2] = m_comps[0];
m_comps[3] = 255;
return *this;
}
inline color_rgba &set(int y, int na)
{
m_comps[0] = static_cast<uint8_t>(clamp<int>(y, 0, 255));
m_comps[1] = m_comps[0];
m_comps[2] = m_comps[0];
m_comps[3] = static_cast<uint8_t>(clamp<int>(na, 0, 255));
return *this;
}
inline color_rgba &set(int sr, int sg, int sb, int sa)
{
m_comps[0] = static_cast<uint8_t>(clamp<int>(sr, 0, 255));
m_comps[1] = static_cast<uint8_t>(clamp<int>(sg, 0, 255));
m_comps[2] = static_cast<uint8_t>(clamp<int>(sb, 0, 255));
m_comps[3] = static_cast<uint8_t>(clamp<int>(sa, 0, 255));
return *this;
}
inline color_rgba &set_rgb(int sr, int sg, int sb)
{
m_comps[0] = static_cast<uint8_t>(clamp<int>(sr, 0, 255));
m_comps[1] = static_cast<uint8_t>(clamp<int>(sg, 0, 255));
m_comps[2] = static_cast<uint8_t>(clamp<int>(sb, 0, 255));
return *this;
}
inline color_rgba &set_rgb(const color_rgba &other)
{
r = other.r;
g = other.g;
b = other.b;
return *this;
}
inline const uint8_t &operator[] (uint32_t index) const { assert(index < 4); return m_comps[index]; }
inline uint8_t &operator[] (uint32_t index) { assert(index < 4); return m_comps[index]; }
inline void clear()
{
m_comps[0] = 0;
m_comps[1] = 0;
m_comps[2] = 0;
m_comps[3] = 0;
}
inline bool operator== (const color_rgba &rhs) const
{
if (m_comps[0] != rhs.m_comps[0]) return false;
if (m_comps[1] != rhs.m_comps[1]) return false;
if (m_comps[2] != rhs.m_comps[2]) return false;
if (m_comps[3] != rhs.m_comps[3]) return false;
return true;
}
inline bool operator!= (const color_rgba &rhs) const
{
return !(*this == rhs);
}
inline bool operator<(const color_rgba &rhs) const
{
for (int i = 0; i < 4; i++)
{
if (m_comps[i] < rhs.m_comps[i])
return true;
else if (m_comps[i] != rhs.m_comps[i])
return false;
}
return false;
}
inline int get_601_luma() const { return (19595U * m_comps[0] + 38470U * m_comps[1] + 7471U * m_comps[2] + 32768U) >> 16U; }
inline int get_709_luma() const { return (13938U * m_comps[0] + 46869U * m_comps[1] + 4729U * m_comps[2] + 32768U) >> 16U; }
inline int get_luma(bool luma_601) const { return luma_601 ? get_601_luma() : get_709_luma(); }
static color_rgba comp_min(const color_rgba& a, const color_rgba& b) { return color_rgba(std::min(a[0], b[0]), std::min(a[1], b[1]), std::min(a[2], b[2]), std::min(a[3], b[3])); }
static color_rgba comp_max(const color_rgba& a, const color_rgba& b) { return color_rgba(std::max(a[0], b[0]), std::max(a[1], b[1]), std::max(a[2], b[2]), std::max(a[3], b[3])); }
};
static_assert(sizeof(color_rgba) == 4);
bool unpack_bc7(const void *pBlock, color_rgba *pPixels);
} // namespace bc7decomp
#ifdef _MSC_VER
#pragma warning(pop)
#endif

254
common/Threading.h Normal file
View File

@@ -0,0 +1,254 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Defs.h"
#if defined(__APPLE__)
#include <mach/semaphore.h>
#elif !defined(_WIN32)
#include <semaphore.h>
#endif
#include <atomic>
#include <functional>
namespace Threading
{
// --------------------------------------------------------------------------------------
// Platform Specific External APIs
// --------------------------------------------------------------------------------------
// The following set of documented functions have Linux/Win32 specific implementations,
// which are found in WinThreads.cpp and LnxThreads.cpp
extern u64 GetThreadCpuTime();
extern u64 GetThreadTicksPerSecond();
/// Set the name of the current thread
extern void SetNameOfCurrentThread(const char* name);
// Releases a timeslice to other threads.
extern void Timeslice();
// For use in spin/wait loops.
extern void SpinWait();
// Optional implementation to enable hires thread/process scheduler for the operating system.
// Needed by Windows, but might not be relevant to other platforms.
extern void EnableHiresScheduler();
extern void DisableHiresScheduler();
// sleeps the current thread for the given number of milliseconds.
extern void Sleep(int ms);
// sleeps the current thread until the specified time point, or later.
extern void SleepUntil(u64 ticks);
// --------------------------------------------------------------------------------------
// ThreadHandle
// --------------------------------------------------------------------------------------
// Abstracts an OS's handle to a thread, closing the handle when necessary. Currently,
// only used for getting the CPU time for a thread.
//
class ThreadHandle
{
public:
ThreadHandle();
ThreadHandle(ThreadHandle&& handle);
ThreadHandle(const ThreadHandle& handle);
~ThreadHandle();
/// Returns a new handle for the calling thread.
static ThreadHandle GetForCallingThread();
ThreadHandle& operator=(ThreadHandle&& handle);
ThreadHandle& operator=(const ThreadHandle& handle);
operator void*() const { return m_native_handle; }
operator bool() const { return (m_native_handle != nullptr); }
/// Returns the amount of CPU time consumed by the thread, at the GetThreadTicksPerSecond() frequency.
u64 GetCPUTime() const;
/// Sets the affinity for a thread to the specified processors.
/// Obviously, only works up to 64 processors.
bool SetAffinity(u64 processor_mask) const;
protected:
void* m_native_handle = nullptr;
// We need the thread ID for affinity adjustments on Linux.
#if defined(__linux__)
unsigned int m_native_id = 0;
#endif
};
// --------------------------------------------------------------------------------------
// Thread
// --------------------------------------------------------------------------------------
// Abstracts a native thread in a lightweight manner. Provides more functionality than
// std::thread (allowing stack size adjustments).
//
class Thread : public ThreadHandle
{
public:
using EntryPoint = std::function<void()>;
Thread();
Thread(Thread&& thread);
Thread(const Thread&) = delete;
Thread(EntryPoint func);
~Thread();
ThreadHandle& operator=(Thread&& thread);
ThreadHandle& operator=(const Thread& handle) = delete;
__fi bool Joinable() const { return (m_native_handle != nullptr); }
__fi u32 GetStackSize() const { return m_stack_size; }
/// Sets the stack size for the thread. Do not call if the thread has already been started.
void SetStackSize(u32 size);
bool Start(EntryPoint func);
void Detach();
void Join();
protected:
#ifdef _WIN32
static unsigned __stdcall ThreadProc(void* param);
#else
static void* ThreadProc(void* param);
#endif
u32 m_stack_size = 0;
};
/// A semaphore that may not have a fast userspace path
/// (Used in other semaphore-based algorithms where the semaphore is just used for its thread sleep/wake ability)
class KernelSemaphore
{
#if defined(_WIN32)
void* m_sema;
#elif defined(__APPLE__)
semaphore_t m_sema;
#else
sem_t m_sema;
#endif
public:
KernelSemaphore();
~KernelSemaphore();
void Post();
void Wait();
bool TryWait();
};
/// A semaphore for notifying a work-processing thread of new work in a (separate) queue
///
/// Usage:
/// - Processing thread loops on `WaitForWork()` followed by processing all work in the queue
/// - Threads adding work first add their work to the queue, then call `NotifyOfWork()`
class WorkSema
{
/// Semaphore for sleeping the worker thread
KernelSemaphore m_sema;
/// Semaphore for sleeping thread waiting on worker queue empty
KernelSemaphore m_empty_sema;
/// Current state (see enum below)
std::atomic<s32> m_state{0};
// Expected call frequency is NotifyOfWork > WaitForWork > WaitForEmpty
// So optimize states for fast NotifyOfWork
enum
{
/* Any <-2 state: STATE_DEAD: Thread has crashed and is awaiting revival */
STATE_SPINNING = -2, ///< Worker thread is spinning waiting for work
STATE_SLEEPING = -1, ///< Worker thread is sleeping on m_sema
STATE_RUNNING_0 = 0, ///< Worker thread is processing work, but no work has been added since it last checked for new work
/* Any >0 state: STATE_RUNNING_N: Worker thread is processing work, and work has been added since it last checked for new work */
STATE_FLAG_WAITING_EMPTY = 1 << 30, ///< Flag to indicate that a thread is sleeping on m_empty_sema (can be applied to any STATE_RUNNING)
};
bool IsDead(s32 state)
{
return state < STATE_SPINNING;
}
bool IsReadyForSleep(s32 state)
{
s32 waiting_empty_cleared = state & (STATE_FLAG_WAITING_EMPTY - 1);
return waiting_empty_cleared == STATE_RUNNING_0;
}
s32 NextStateWaitForWork(s32 current)
{
s32 new_state = IsReadyForSleep(current) ? STATE_SLEEPING : STATE_RUNNING_0;
return new_state | (current & STATE_FLAG_WAITING_EMPTY); // Preserve waiting empty flag for RUNNING_N → RUNNING_0
}
public:
/// Notify the worker thread that you've added new work to its queue
void NotifyOfWork()
{
// State change:
// DEAD: Stay in DEAD (starting DEAD state is INT_MIN so we can assume we won't flip over to anything else)
// SPINNING: Change state to RUNNING. Thread will notice and process the new data
// SLEEPING: Change state to RUNNING and wake worker. Thread will wake up and process the new data.
// RUNNING_0: Change state to RUNNING_N.
// RUNNING_N: Stay in RUNNING_N
s32 old = m_state.fetch_add(2, std::memory_order_release);
if (old == STATE_SLEEPING)
m_sema.Post();
}
/// Checks if there's any work in the queue
bool CheckForWork();
/// Wait for work to be added to the queue
void WaitForWork();
/// Wait for work to be added to the queue, spinning for a bit before sleeping the thread
void WaitForWorkWithSpin();
/// Wait for the worker thread to finish processing all entries in the queue or die
/// Returns false if the thread is dead
bool WaitForEmpty();
/// Wait for the worker thread to finish processing all entries in the queue or die, spinning a bit before sleeping the thread
/// Returns false if the thread is dead
bool WaitForEmptyWithSpin();
/// Called by the worker thread to notify others of its death
/// Dead threads don't process work, and WaitForEmpty will return instantly even though there may be work in the queue
void Kill();
/// Reset the semaphore to the initial state
/// Should be called by the worker thread if it restarts after dying
void Reset();
};
/// A semaphore that definitely has a fast userspace path
class UserspaceSemaphore
{
KernelSemaphore m_sema;
std::atomic<int32_t> m_counter{0};
public:
UserspaceSemaphore() = default;
~UserspaceSemaphore() = default;
void Post()
{
if (m_counter.fetch_add(1, std::memory_order_release) < 0)
m_sema.Post();
}
void Wait()
{
if (m_counter.fetch_sub(1, std::memory_order_acquire) <= 0)
m_sema.Wait();
}
bool TryWait()
{
int32_t counter = m_counter.load(std::memory_order_relaxed);
while (counter > 0 && !m_counter.compare_exchange_weak(counter, counter - 1, std::memory_order_acquire, std::memory_order_relaxed))
;
return counter > 0;
}
};
} // namespace Threading

186
common/Timer.cpp Normal file
View File

@@ -0,0 +1,186 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Timer.h"
#include <cstdio>
#include <cstdlib>
#if defined(_WIN32)
#include "RedtapeWindows.h"
#else
#include <time.h>
#endif
namespace Common
{
#ifdef _WIN32
static double s_counter_frequency;
static bool s_counter_initialized = false;
Timer::Value Timer::GetCurrentValue()
{
// even if this races, it should still result in the same value..
if (!s_counter_initialized)
{
LARGE_INTEGER Freq;
QueryPerformanceFrequency(&Freq);
s_counter_frequency = static_cast<double>(Freq.QuadPart) / 1000000000.0;
s_counter_initialized = true;
}
Timer::Value ReturnValue;
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&ReturnValue));
return ReturnValue;
}
double Timer::ConvertValueToNanoseconds(Timer::Value value)
{
return (static_cast<double>(value) / s_counter_frequency);
}
double Timer::ConvertValueToMilliseconds(Timer::Value value)
{
return ((static_cast<double>(value) / s_counter_frequency) / 1000000.0);
}
double Timer::ConvertValueToSeconds(Timer::Value value)
{
return ((static_cast<double>(value) / s_counter_frequency) / 1000000000.0);
}
Timer::Value Timer::ConvertSecondsToValue(double s)
{
return static_cast<Value>((s * 1000000000.0) * s_counter_frequency);
}
Timer::Value Timer::ConvertMillisecondsToValue(double ms)
{
return static_cast<Value>((ms * 1000000.0) * s_counter_frequency);
}
Timer::Value Timer::ConvertNanosecondsToValue(double ns)
{
return static_cast<Value>(ns * s_counter_frequency);
}
#else
Timer::Value Timer::GetCurrentValue()
{
struct timespec tv;
clock_gettime(CLOCK_MONOTONIC, &tv);
return ((Value)tv.tv_nsec + (Value)tv.tv_sec * 1000000000);
}
double Timer::ConvertValueToNanoseconds(Timer::Value value)
{
return static_cast<double>(value);
}
double Timer::ConvertValueToMilliseconds(Timer::Value value)
{
return (static_cast<double>(value) / 1000000.0);
}
double Timer::ConvertValueToSeconds(Timer::Value value)
{
return (static_cast<double>(value) / 1000000000.0);
}
Timer::Value Timer::ConvertSecondsToValue(double s)
{
return static_cast<Value>(s * 1000000000.0);
}
Timer::Value Timer::ConvertMillisecondsToValue(double ms)
{
return static_cast<Value>(ms * 1000000.0);
}
Timer::Value Timer::ConvertNanosecondsToValue(double ns)
{
return static_cast<Value>(ns);
}
#endif
Timer::Timer()
{
Reset();
}
void Timer::Reset()
{
m_tvStartValue = GetCurrentValue();
}
double Timer::GetTimeSeconds() const
{
return ConvertValueToSeconds(GetCurrentValue() - m_tvStartValue);
}
double Timer::GetTimeMilliseconds() const
{
return ConvertValueToMilliseconds(GetCurrentValue() - m_tvStartValue);
}
double Timer::GetTimeNanoseconds() const
{
return ConvertValueToNanoseconds(GetCurrentValue() - m_tvStartValue);
}
double Timer::GetTimeSecondsAndReset()
{
const Value value = GetCurrentValue();
const double ret = ConvertValueToSeconds(value - m_tvStartValue);
m_tvStartValue = value;
return ret;
}
double Timer::GetTimeMillisecondsAndReset()
{
const Value value = GetCurrentValue();
const double ret = ConvertValueToMilliseconds(value - m_tvStartValue);
m_tvStartValue = value;
return ret;
}
double Timer::GetTimeNanosecondsAndReset()
{
const Value value = GetCurrentValue();
const double ret = ConvertValueToNanoseconds(value - m_tvStartValue);
m_tvStartValue = value;
return ret;
}
bool Timer::ResetIfSecondsPassed(double s)
{
const Value value = GetCurrentValue();
const double ret = ConvertValueToSeconds(value - m_tvStartValue);
if (ret < s)
return false;
m_tvStartValue = value;
return true;
}
bool Timer::ResetIfMillisecondsPassed(double s)
{
const Value value = GetCurrentValue();
const double ret = ConvertValueToMilliseconds(value - m_tvStartValue);
if (ret < s)
return false;
m_tvStartValue = value;
return true;
}
bool Timer::ResetIfNanosecondsPassed(double s)
{
const Value value = GetCurrentValue();
const double ret = ConvertValueToNanoseconds(value - m_tvStartValue);
if (ret < s)
return false;
m_tvStartValue = value;
return true;
}
} // namespace Common

44
common/Timer.h Normal file
View File

@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <cstdint>
namespace Common
{
class Timer
{
public:
using Value = std::uint64_t;
Timer();
static Value GetCurrentValue();
static double ConvertValueToSeconds(Value value);
static double ConvertValueToMilliseconds(Value value);
static double ConvertValueToNanoseconds(Value value);
static Value ConvertSecondsToValue(double s);
static Value ConvertMillisecondsToValue(double s);
static Value ConvertNanosecondsToValue(double ns);
void Reset();
void ResetTo(Value value) { m_tvStartValue = value; }
Value GetStartValue() const { return m_tvStartValue; }
double GetTimeSeconds() const;
double GetTimeMilliseconds() const;
double GetTimeNanoseconds() const;
double GetTimeSecondsAndReset();
double GetTimeMillisecondsAndReset();
double GetTimeNanosecondsAndReset();
bool ResetIfSecondsPassed(double s);
bool ResetIfMillisecondsPassed(double s);
bool ResetIfNanosecondsPassed(double s);
private:
Value m_tvStartValue;
};
} // namespace Common

46
common/VectorIntrin.h Normal file
View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// Includes appropriate intrinsic header based on platform.
#pragma once
#if defined(_M_X86)
#ifdef _MSC_VER
#include <intrin.h>
#endif
#if defined(__AVX2__)
#define _M_SSE 0x501
#elif defined(__AVX__)
#define _M_SSE 0x500
#elif defined(__SSE4_1__)
#define _M_SSE 0x401
#else
#error PCSX2 requires compiling for at least SSE 4.1
#endif
// Starting with AVX, processors have fast unaligned loads
// Reduce code duplication by not compiling multiple versions
#if _M_SSE >= 0x500
#define FAST_UNALIGNED 1
#else
#define FAST_UNALIGNED 0
#endif
#include <xmmintrin.h>
#include <emmintrin.h>
#include <tmmintrin.h>
#include <smmintrin.h>
#include <immintrin.h>
#elif defined(_M_ARM64)
#include <arm_neon.h>
#endif
#ifdef __APPLE__
#include <stdlib.h> // alloca
#else
#include <malloc.h> // alloca
#endif

115
common/WAVWriter.cpp Normal file
View File

@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/WAVWriter.h"
#include "common/FileSystem.h"
#include "common/Console.h"
#pragma pack(push, 1)
struct WAV_HEADER
{
u32 chunk_id; // RIFF
u32 chunk_size;
u32 format; // WAVE
struct FormatChunk
{
u32 chunk_id; // "fmt "
u32 chunk_size;
u16 audio_format; // pcm = 1
u16 num_channels;
u32 sample_rate;
u32 byte_rate;
u16 block_align;
u16 bits_per_sample;
} fmt_chunk;
struct DataChunkHeader
{
u32 chunk_id; // "data "
u32 chunk_size;
} data_chunk_header;
};
#pragma pack(pop)
using namespace Common;
WAVWriter::WAVWriter() = default;
WAVWriter::~WAVWriter()
{
if (IsOpen())
Close();
}
bool WAVWriter::Open(const char* filename, u32 sample_rate, u32 num_channels)
{
if (IsOpen())
Close();
m_file = FileSystem::OpenCFile(filename, "wb");
if (!m_file)
return false;
m_sample_rate = sample_rate;
m_num_channels = num_channels;
if (!WriteHeader())
{
Console.Error("Failed to write header to file");
m_sample_rate = 0;
m_num_channels = 0;
std::fclose(m_file);
m_file = nullptr;
return false;
}
return true;
}
void WAVWriter::Close()
{
if (!IsOpen())
return;
if (std::fseek(m_file, 0, SEEK_SET) != 0 || !WriteHeader())
Console.Error("Failed to re-write header on file, file may be unplayable");
std::fclose(m_file);
m_file = nullptr;
m_sample_rate = 0;
m_num_channels = 0;
m_num_frames = 0;
}
void WAVWriter::WriteFrames(const s16* samples, u32 num_frames)
{
const u32 num_frames_written =
static_cast<u32>(std::fwrite(samples, sizeof(s16) * m_num_channels, num_frames, m_file));
if (num_frames_written != num_frames)
Console.Error("Only wrote %u of %u frames to output file", num_frames_written, num_frames);
m_num_frames += num_frames_written;
}
bool WAVWriter::WriteHeader()
{
const u32 data_size = sizeof(SampleType) * m_num_channels * m_num_frames;
WAV_HEADER header = {};
header.chunk_id = 0x46464952; // 0x52494646
header.chunk_size = sizeof(WAV_HEADER) - 8 + data_size;
header.format = 0x45564157; // 0x57415645
header.fmt_chunk.chunk_id = 0x20746d66; // 0x666d7420
header.fmt_chunk.chunk_size = sizeof(header.fmt_chunk) - 8;
header.fmt_chunk.audio_format = 1;
header.fmt_chunk.num_channels = static_cast<u16>(m_num_channels);
header.fmt_chunk.sample_rate = m_sample_rate;
header.fmt_chunk.byte_rate = m_sample_rate * m_num_channels * sizeof(SampleType);
header.fmt_chunk.block_align = static_cast<u16>(m_num_channels * sizeof(SampleType));
header.fmt_chunk.bits_per_sample = 16;
header.data_chunk_header.chunk_id = 0x61746164; // 0x64617461
header.data_chunk_header.chunk_size = data_size;
return (std::fwrite(&header, sizeof(header), 1, m_file) == 1);
}

36
common/WAVWriter.h Normal file
View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Defs.h"
#include <cstdio>
namespace Common
{
class WAVWriter
{
public:
WAVWriter();
~WAVWriter();
__fi u32 GetSampleRate() const { return m_sample_rate; }
__fi u32 GetNumChannels() const { return m_num_channels; }
__fi u32 GetNumFrames() const { return m_num_frames; }
__fi bool IsOpen() const { return (m_file != nullptr); }
bool Open(const char* filename, u32 sample_rate, u32 num_channels);
void Close();
void WriteFrames(const s16* samples, u32 num_frames);
private:
using SampleType = s16;
bool WriteHeader();
std::FILE* m_file = nullptr;
u32 m_sample_rate = 0;
u32 m_num_channels = 0;
u32 m_num_frames = 0;
};
} // namespace Common

257
common/WindowInfo.cpp Normal file
View File

@@ -0,0 +1,257 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "WindowInfo.h"
#include "Console.h"
#include "Error.h"
#include "HeapArray.h"
#if defined(_WIN32)
#include "RedtapeWindows.h"
#include <dwmapi.h>
static std::optional<float> GetRefreshRateFromDisplayConfig(HWND hwnd)
{
// Partially based on Chromium ui/display/win/display_config_helper.cc.
const HMONITOR monitor = MonitorFromWindow(hwnd, 0);
if (!monitor) [[unlikely]]
{
ERROR_LOG("{}() failed: {}", "MonitorFromWindow", Error::CreateWin32(GetLastError()).GetDescription());
return std::nullopt;
}
MONITORINFOEXW mi = {};
mi.cbSize = sizeof(mi);
if (!GetMonitorInfoW(monitor, &mi))
{
ERROR_LOG("{}() failed: {}", "GetMonitorInfoW", Error::CreateWin32(GetLastError()).GetDescription());
return std::nullopt;
}
DynamicHeapArray<DISPLAYCONFIG_PATH_INFO> path_info;
DynamicHeapArray<DISPLAYCONFIG_MODE_INFO> mode_info;
// I guess this could fail if it changes inbetween two calls... unlikely.
for (;;)
{
UINT32 path_size = 0, mode_size = 0;
LONG res = GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_size, &mode_size);
if (res != ERROR_SUCCESS)
{
ERROR_LOG("{}() failed: {}", "GetDisplayConfigBufferSizes", Error::CreateWin32(res).GetDescription());
return std::nullopt;
}
path_info.resize(path_size);
mode_info.resize(mode_size);
res =
QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_size, path_info.data(), &mode_size, mode_info.data(), nullptr);
if (res == ERROR_SUCCESS)
break;
if (res != ERROR_INSUFFICIENT_BUFFER)
{
ERROR_LOG("{}() failed: {}", "QueryDisplayConfig", Error::CreateWin32(res).GetDescription());
return std::nullopt;
}
}
for (const DISPLAYCONFIG_PATH_INFO& pi : path_info)
{
DISPLAYCONFIG_SOURCE_DEVICE_NAME sdn = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME),
.adapterId = pi.sourceInfo.adapterId,
.id = pi.sourceInfo.id}};
LONG res = DisplayConfigGetDeviceInfo(&sdn.header);
if (res != ERROR_SUCCESS)
{
ERROR_LOG("{}() failed: {}", "DisplayConfigGetDeviceInfo", Error::CreateWin32(res).GetDescription());
continue;
}
if (std::wcscmp(sdn.viewGdiDeviceName, mi.szDevice) == 0)
{
// Found the monitor!
return static_cast<float>(static_cast<double>(pi.targetInfo.refreshRate.Numerator) /
static_cast<double>(pi.targetInfo.refreshRate.Denominator));
}
}
return std::nullopt;
}
static std::optional<float> GetRefreshRateFromDWM(HWND hwnd)
{
BOOL composition_enabled;
if (FAILED(DwmIsCompositionEnabled(&composition_enabled)))
return std::nullopt;
DWM_TIMING_INFO ti = {};
ti.cbSize = sizeof(ti);
HRESULT hr = DwmGetCompositionTimingInfo(nullptr, &ti);
if (SUCCEEDED(hr))
{
if (ti.rateRefresh.uiNumerator == 0 || ti.rateRefresh.uiDenominator == 0)
return std::nullopt;
return static_cast<float>(ti.rateRefresh.uiNumerator) / static_cast<float>(ti.rateRefresh.uiDenominator);
}
return std::nullopt;
}
static std::optional<float> GetRefreshRateFromMonitor(HWND hwnd)
{
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
if (!mon)
return std::nullopt;
MONITORINFOEXW mi = {};
mi.cbSize = sizeof(mi);
if (GetMonitorInfoW(mon, &mi))
{
DEVMODEW dm = {};
dm.dmSize = sizeof(dm);
// 0/1 are reserved for "defaults".
if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
return static_cast<float>(dm.dmDisplayFrequency);
}
return std::nullopt;
}
std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
std::optional<float> ret;
if (wi.type != Type::Win32 || !wi.window_handle)
return ret;
// Try DWM first, then fall back to integer values.
const HWND hwnd = static_cast<HWND>(wi.window_handle);
ret = GetRefreshRateFromDisplayConfig(hwnd);
if (!ret.has_value())
{
ret = GetRefreshRateFromDWM(hwnd);
if (!ret.has_value())
ret = GetRefreshRateFromMonitor(hwnd);
}
return ret;
}
#elif defined(__APPLE__)
#include "common/CocoaTools.h"
std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
if (wi.type == WindowInfo::Type::MacOS)
return CocoaTools::GetViewRefreshRate(wi);
return std::nullopt;
}
#else
#if defined(X11_API)
#include "common/ScopedGuard.h"
#include <X11/extensions/Xrandr.h>
#include <X11/Xlib.h>
static std::optional<float> GetRefreshRateFromXRandR(const WindowInfo& wi)
{
Display* display = static_cast<Display*>(wi.display_connection);
Window window = static_cast<Window>(reinterpret_cast<uintptr_t>(wi.window_handle));
if (!display || !window)
return std::nullopt;
XRRScreenResources* res = XRRGetScreenResources(display, window);
if (!res)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetScreenResources() failed");
return std::nullopt;
}
ScopedGuard res_guard([res]() { XRRFreeScreenResources(res); });
int num_monitors;
XRRMonitorInfo* mi = XRRGetMonitors(display, window, True, &num_monitors);
if (num_monitors < 0)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetMonitors() failed");
return std::nullopt;
}
else if (num_monitors > 1)
{
Console.Warning("(GetRefreshRateFromXRandR) XRRGetMonitors() returned %d monitors, using first", num_monitors);
}
ScopedGuard mi_guard([mi]() { XRRFreeMonitors(mi); });
if (mi->noutput <= 0)
{
Console.Error("(GetRefreshRateFromXRandR) Monitor has no outputs");
return std::nullopt;
}
else if (mi->noutput > 1)
{
Console.Warning("(GetRefreshRateFromXRandR) Monitor has %d outputs, using first", mi->noutput);
}
XRROutputInfo* oi = XRRGetOutputInfo(display, res, mi->outputs[0]);
if (!oi)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetOutputInfo() failed");
return std::nullopt;
}
ScopedGuard oi_guard([oi]() { XRRFreeOutputInfo(oi); });
XRRCrtcInfo* ci = XRRGetCrtcInfo(display, res, oi->crtc);
if (!ci)
{
Console.Error("(GetRefreshRateFromXRandR) XRRGetCrtcInfo() failed");
return std::nullopt;
}
ScopedGuard ci_guard([ci]() { XRRFreeCrtcInfo(ci); });
XRRModeInfo* mode = nullptr;
for (int i = 0; i < res->nmode; i++)
{
if (res->modes[i].id == ci->mode)
{
mode = &res->modes[i];
break;
}
}
if (!mode)
{
Console.Error("(GetRefreshRateFromXRandR) Failed to look up mode %d (of %d)", static_cast<int>(ci->mode), res->nmode);
return std::nullopt;
}
if (mode->dotClock == 0 || mode->hTotal == 0 || mode->vTotal == 0)
{
Console.Error("(GetRefreshRateFromXRandR) Modeline is invalid: %ld/%d/%d", mode->dotClock, mode->hTotal, mode->vTotal);
return std::nullopt;
}
return static_cast<float>(
static_cast<double>(mode->dotClock) / (static_cast<double>(mode->hTotal) * static_cast<double>(mode->vTotal)));
}
#endif // X11_API
std::optional<float> WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi)
{
#if defined(X11_API)
if (wi.type == WindowInfo::Type::X11)
return GetRefreshRateFromXRandR(wi);
#endif
return std::nullopt;
}
#endif

47
common/WindowInfo.h Normal file
View File

@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Pcsx2Defs.h"
#include <optional>
/// Contains the information required to create a graphics context in a window.
struct WindowInfo
{
enum class Type
{
Surfaceless,
Win32,
X11,
Wayland,
MacOS
};
/// The type of the surface. Surfaceless indicates it will not be displayed on screen at all.
Type type = Type::Surfaceless;
/// Connection to the display server. On most platforms except X11/Wayland, this is implicit and null.
void* display_connection = nullptr;
/// Abstract handle to the window. This depends on the surface type.
void* window_handle = nullptr;
/// For platforms where a separate surface/layer handle is needed, it is stored here (e.g. MacOS).
void* surface_handle = nullptr;
/// Width of the surface in pixels.
u32 surface_width = 0;
/// Height of the surface in pixels.
u32 surface_height = 0;
/// DPI scale for the surface.
float surface_scale = 1.0f;
/// Refresh rate of the surface, if available.
float surface_refresh_rate = 0.0f;
/// Returns the host's refresh rate for the given window, if available.
static std::optional<float> QueryRefreshRateForWindow(const WindowInfo& wi);
};

View File

@@ -0,0 +1,375 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/HostSys.h"
#include "common/AlignedMalloc.h"
#include "common/Assertions.h"
#include "common/BitUtils.h"
#include "common/Console.h"
#include "common/Error.h"
#include "common/RedtapeWindows.h"
#include "common/StringUtil.h"
#include "fmt/format.h"
#include <mutex>
static DWORD ConvertToWinApi(const PageProtectionMode& mode)
{
DWORD winmode = PAGE_NOACCESS;
// Windows has some really bizarre memory protection enumeration that uses bitwise
// numbering (like flags) but is in fact not a flag value. *Someone* from the early
// microsoft days wasn't a very good coder, me thinks. --air
if (mode.CanExecute())
{
winmode = mode.CanWrite() ? PAGE_EXECUTE_READWRITE : PAGE_EXECUTE_READ;
}
else if (mode.CanRead())
{
winmode = mode.CanWrite() ? PAGE_READWRITE : PAGE_READONLY;
}
return winmode;
}
void* HostSys::Mmap(void* base, size_t size, const PageProtectionMode& mode)
{
if (mode.IsNone())
return nullptr;
return VirtualAlloc(base, size, MEM_RESERVE | MEM_COMMIT, ConvertToWinApi(mode));
}
void HostSys::Munmap(void* base, size_t size)
{
if (!base)
return;
VirtualFree((void*)base, 0, MEM_RELEASE);
}
void HostSys::MemProtect(void* baseaddr, size_t size, const PageProtectionMode& mode)
{
pxAssert((size & (__pagesize - 1)) == 0);
DWORD OldProtect; // enjoy my uselessness, yo!
if (!VirtualProtect(baseaddr, size, ConvertToWinApi(mode), &OldProtect))
pxFail("VirtualProtect() failed");
}
std::string HostSys::GetFileMappingName(const char* prefix)
{
const unsigned pid = GetCurrentProcessId();
return fmt::format("{}_{}", prefix, pid);
}
void* HostSys::CreateSharedMemory(const char* name, size_t size)
{
return static_cast<void*>(CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
static_cast<DWORD>(size >> 32), static_cast<DWORD>(size), StringUtil::UTF8StringToWideString(name).c_str()));
}
void HostSys::DestroySharedMemory(void* ptr)
{
CloseHandle(static_cast<HANDLE>(ptr));
}
void* HostSys::MapSharedMemory(void* handle, size_t offset, void* baseaddr, size_t size, const PageProtectionMode& mode)
{
void* ret = MapViewOfFileEx(static_cast<HANDLE>(handle), FILE_MAP_READ | FILE_MAP_WRITE,
static_cast<DWORD>(offset >> 32), static_cast<DWORD>(offset), size, baseaddr);
if (!ret)
return nullptr;
const DWORD prot = ConvertToWinApi(mode);
if (prot != PAGE_READWRITE)
{
DWORD old_prot;
if (!VirtualProtect(ret, size, prot, &old_prot))
pxFail("Failed to protect memory mapping");
}
return ret;
}
void HostSys::UnmapSharedMemory(void* baseaddr, size_t size)
{
if (!UnmapViewOfFile(baseaddr))
pxFail("Failed to unmap shared memory");
}
size_t HostSys::GetRuntimePageSize()
{
SYSTEM_INFO si = {};
GetSystemInfo(&si);
return si.dwPageSize;
}
size_t HostSys::GetRuntimeCacheLineSize()
{
DWORD size = 0;
if (!GetLogicalProcessorInformation(nullptr, &size) && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
return 0;
std::unique_ptr<SYSTEM_LOGICAL_PROCESSOR_INFORMATION[]> lpi =
std::make_unique<SYSTEM_LOGICAL_PROCESSOR_INFORMATION[]>(
(size + (sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION) - 1)) / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION));
if (!GetLogicalProcessorInformation(lpi.get(), &size))
return 0;
u32 max_line_size = 0;
for (u32 i = 0; i < size / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); i++)
{
if (lpi[i].Relationship == RelationCache)
max_line_size = std::max<u32>(max_line_size, lpi[i].Cache.LineSize);
}
return max_line_size;
}
#ifdef _M_ARM64
void HostSys::FlushInstructionCache(void* address, u32 size)
{
::FlushInstructionCache(GetCurrentProcess(), address, size);
}
#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)
{
m_placeholder_ranges.emplace(0, size);
}
SharedMemoryMappingArea::~SharedMemoryMappingArea()
{
pxAssertRel(m_num_mappings == 0, "No mappings left");
// hopefully this will be okay, and we don't need to coalesce all the placeholders...
if (!VirtualFreeEx(GetCurrentProcess(), m_base_ptr, 0, MEM_RELEASE))
pxFailRel("Failed to release shared memory area");
}
SharedMemoryMappingArea::PlaceholderMap::iterator SharedMemoryMappingArea::FindPlaceholder(size_t offset)
{
if (m_placeholder_ranges.empty())
return m_placeholder_ranges.end();
// this will give us an iterator equal or after page
auto it = m_placeholder_ranges.lower_bound(offset);
if (it == m_placeholder_ranges.end())
{
// check the last page
it = (++m_placeholder_ranges.rbegin()).base();
}
// it's the one we found?
if (offset >= it->first && offset < it->second)
return it;
// otherwise try the one before
if (it == m_placeholder_ranges.begin())
return m_placeholder_ranges.end();
--it;
if (offset >= it->first && offset < it->second)
return it;
else
return m_placeholder_ranges.end();
}
std::unique_ptr<SharedMemoryMappingArea> SharedMemoryMappingArea::Create(size_t size)
{
pxAssertRel(Common::IsAlignedPow2(size, __pagesize), "Size is page aligned");
void* alloc = VirtualAlloc2(GetCurrentProcess(), nullptr, size, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0);
if (!alloc)
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));
const size_t map_offset = static_cast<u8*>(map_base) - m_base_ptr;
pxAssert(Common::IsAlignedPow2(map_offset, __pagesize));
pxAssert(Common::IsAlignedPow2(map_size, __pagesize));
// should be a placeholder. unless there's some other mapping we didn't free.
PlaceholderMap::iterator phit = FindPlaceholder(map_offset);
pxAssertMsg(phit != m_placeholder_ranges.end(), "Page we're mapping is a placeholder");
pxAssertMsg(map_offset >= phit->first && map_offset < phit->second, "Page is in returned placeholder range");
pxAssertMsg((map_offset + map_size) <= phit->second, "Page range is in returned placeholder range");
// do we need to split to the left? (i.e. is there a placeholder before this range)
const size_t old_ph_end = phit->second;
if (map_offset != phit->first)
{
phit->second = map_offset;
// split it (i.e. left..start and start..end are now separated)
if (!VirtualFreeEx(GetCurrentProcess(), OffsetPointer(phit->first),
(map_offset - phit->first), MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER))
{
pxFailRel("Failed to left split placeholder for map");
}
}
else
{
// start of the placeholder is getting used, we'll split it right below if there's anything left over
m_placeholder_ranges.erase(phit);
}
// do we need to split to the right? (i.e. is there a placeholder after this range)
if ((map_offset + map_size) != old_ph_end)
{
// split out end..ph_end
m_placeholder_ranges.emplace(map_offset + map_size, old_ph_end);
if (!VirtualFreeEx(GetCurrentProcess(), OffsetPointer(map_offset), map_size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER))
{
pxFailRel("Failed to right split placeholder for map");
}
}
// actually do the mapping, replacing the placeholder on the range
if (!MapViewOfFile3(static_cast<HANDLE>(file_handle), GetCurrentProcess(),
map_base, file_offset, map_size, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0))
{
Console.Error("(SharedMemoryMappingArea) MapViewOfFile3() failed: %u", GetLastError());
return nullptr;
}
const DWORD prot = ConvertToWinApi(mode);
if (prot != PAGE_READWRITE)
{
DWORD old_prot;
if (!VirtualProtect(map_base, map_size, prot, &old_prot))
pxFail("Failed to protect memory mapping");
}
m_num_mappings++;
return static_cast<u8*>(map_base);
}
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));
const size_t map_offset = static_cast<u8*>(map_base) - m_base_ptr;
pxAssert(Common::IsAlignedPow2(map_offset, __pagesize));
pxAssert(Common::IsAlignedPow2(map_size, __pagesize));
// unmap the specified range
if (!UnmapViewOfFile2(GetCurrentProcess(), map_base, MEM_PRESERVE_PLACEHOLDER))
{
Console.Error("(SharedMemoryMappingArea) UnmapViewOfFile2() failed: %u", GetLastError());
return false;
}
// can we coalesce to the left?
PlaceholderMap::iterator left_it = (map_offset > 0) ? FindPlaceholder(map_offset - 1) : m_placeholder_ranges.end();
if (left_it != m_placeholder_ranges.end())
{
// the left placeholder should end at our start
pxAssert(map_offset == left_it->second);
left_it->second = map_offset + map_size;
// combine placeholders before and the range we're unmapping, i.e. to the left
if (!VirtualFreeEx(GetCurrentProcess(), OffsetPointer(left_it->first),
left_it->second - left_it->first, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS))
{
pxFail("Failed to coalesce placeholders left for unmap");
}
}
else
{
// this is a new placeholder
left_it = m_placeholder_ranges.emplace(map_offset, map_offset + map_size).first;
}
// can we coalesce to the right?
PlaceholderMap::iterator right_it = ((map_offset + map_size) < m_size) ? FindPlaceholder(map_offset + map_size) : m_placeholder_ranges.end();
if (right_it != m_placeholder_ranges.end())
{
// should start at our end
pxAssert(right_it->first == (map_offset + map_size));
left_it->second = right_it->second;
m_placeholder_ranges.erase(right_it);
// combine our placeholder and the next, i.e. to the right
if (!VirtualFreeEx(GetCurrentProcess(), OffsetPointer(left_it->first),
left_it->second - left_it->first, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS))
{
pxFail("Failed to coalescae placeholders right for unmap");
}
}
m_num_mappings--;
return true;
}
namespace PageFaultHandler
{
static LONG ExceptionHandler(PEXCEPTION_POINTERS exi);
static std::recursive_mutex s_exception_handler_mutex;
static bool s_in_exception_handler = false;
static bool s_installed = false;
} // namespace PageFaultHandler
LONG PageFaultHandler::ExceptionHandler(PEXCEPTION_POINTERS exi)
{
// Executing the handler concurrently from multiple threads wouldn't go down well.
std::unique_lock lock(s_exception_handler_mutex);
// Prevent recursive exception filtering.
if (s_in_exception_handler)
return EXCEPTION_CONTINUE_SEARCH;
// Only interested in page faults.
if (exi->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION)
return EXCEPTION_CONTINUE_SEARCH;
#if defined(_M_X86)
void* const exception_pc = reinterpret_cast<void*>(exi->ContextRecord->Rip);
#elif defined(_M_ARM64)
void* const exception_pc = reinterpret_cast<void*>(exi->ContextRecord->Pc);
#else
void* const exception_pc = nullptr;
#endif
void* const exception_address = reinterpret_cast<void*>(exi->ExceptionRecord->ExceptionInformation[1]);
const bool is_write = exi->ExceptionRecord->ExceptionInformation[0] == 1;
s_in_exception_handler = true;
const HandlerResult handled = HandlePageFault(exception_pc, exception_address, is_write);
s_in_exception_handler = false;
return (handled == HandlerResult::ContinueExecution) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH;
}
bool PageFaultHandler::Install(Error* error)
{
std::unique_lock lock(s_exception_handler_mutex);
pxAssertRel(!s_installed, "Page fault handler has already been installed.");
PVOID handle = AddVectoredExceptionHandler(1, ExceptionHandler);
if (!handle)
{
Error::SetWin32(error, "AddVectoredExceptionHandler() failed: ", GetLastError());
return false;
}
s_installed = true;
return true;
}

188
common/Windows/WinMisc.cpp Normal file
View File

@@ -0,0 +1,188 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/Console.h"
#include "common/FileSystem.h"
#include "common/HostSys.h"
#include "common/RedtapeWindows.h"
#include "common/StringUtil.h"
#include "common/Threading.h"
#include "common/WindowInfo.h"
#include <mmsystem.h>
#include <timeapi.h>
#include <VersionHelpers.h>
// If anything tries to read this as an initializer, we're in trouble.
static const LARGE_INTEGER lfreq = []() {
LARGE_INTEGER ret = {};
QueryPerformanceFrequency(&ret);
return ret;
}();
// This gets leaked... oh well.
static thread_local HANDLE s_sleep_timer;
static thread_local bool s_sleep_timer_created = false;
static HANDLE GetSleepTimer()
{
if (s_sleep_timer_created)
return s_sleep_timer;
s_sleep_timer_created = true;
s_sleep_timer = CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
if (!s_sleep_timer)
s_sleep_timer = CreateWaitableTimer(nullptr, TRUE, nullptr);
return s_sleep_timer;
}
u64 GetTickFrequency()
{
return lfreq.QuadPart;
}
u64 GetCPUTicks()
{
LARGE_INTEGER count;
QueryPerformanceCounter(&count);
return count.QuadPart;
}
u64 GetPhysicalMemory()
{
MEMORYSTATUSEX status;
status.dwLength = sizeof(status);
GlobalMemoryStatusEx(&status);
return status.ullTotalPhys;
}
u64 GetAvailablePhysicalMemory()
{
MEMORYSTATUSEX status;
status.dwLength = sizeof(status);
GlobalMemoryStatusEx(&status);
return status.ullAvailPhys;
}
// Calculates the Windows OS Version and processor architecture, and returns it as a
// human-readable string. :)
std::string GetOSVersionString()
{
std::string retval;
SYSTEM_INFO si;
GetNativeSystemInfo(&si);
if (IsWindows10OrGreater())
{
retval = "Microsoft ";
retval += IsWindowsServer() ? "Windows Server 2016+" : "Windows 10+";
}
else
retval = "Unsupported Operating System!";
return retval;
}
bool Common::InhibitScreensaver(bool inhibit)
{
EXECUTION_STATE flags = ES_CONTINUOUS;
if (inhibit)
flags |= ES_DISPLAY_REQUIRED;
SetThreadExecutionState(flags);
return true;
}
void Common::SetMousePosition(int x, int y)
{
SetCursorPos(x, y);
}
/*
static HHOOK mouseHook = nullptr;
static std::function<void(int, int)> fnMouseMoveCb;
LRESULT CALLBACK Mousecb(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0 && wParam == WM_MOUSEMOVE)
{
MSLLHOOKSTRUCT* mouse = (MSLLHOOKSTRUCT*)lParam;
fnMouseMoveCb(mouse->pt.x, mouse->pt.y);
}
return CallNextHookEx(mouseHook, nCode, wParam, lParam);
}
*/
// This (and the above) works, but is not recommended on Windows and is only here for consistency.
// Defer to using raw input instead.
bool Common::AttachMousePositionCb(std::function<void(int, int)> cb)
{
/*
if (mouseHook)
Common::DetachMousePositionCb();
fnMouseMoveCb = cb;
mouseHook = SetWindowsHookEx(WH_MOUSE_LL, Mousecb, GetModuleHandle(NULL), 0);
if (!mouseHook)
{
Console.Warning("Failed to set mouse hook: %d", GetLastError());
return false;
}
#if defined(PCSX2_DEBUG) || defined(PCSX2_DEVBUILD)
static bool warned = false;
if (!warned)
{
Console.Warning("Mouse hooks are enabled, and this isn't a release build! Using a debugger, or loading symbols, _will_ stall the hook and cause global mouse lag.");
warned = true;
}
#endif
*/
return true;
}
void Common::DetachMousePositionCb()
{
/*
UnhookWindowsHookEx(mouseHook);
mouseHook = nullptr;
*/
}
bool Common::PlaySoundAsync(const char* path)
{
const std::wstring wpath = FileSystem::GetWin32Path(path);
return PlaySoundW(wpath.c_str(), NULL, SND_ASYNC | SND_NODEFAULT);
}
void Threading::Sleep(int ms)
{
::Sleep(ms);
}
void Threading::SleepUntil(u64 ticks)
{
// This is definitely sub-optimal, but there's no way to sleep until a QPC timestamp on Win32.
const s64 diff = static_cast<s64>(ticks - GetCPUTicks());
if (diff <= 0)
return;
const HANDLE hTimer = GetSleepTimer();
if (!hTimer)
return;
const u64 one_hundred_nanos_diff = (static_cast<u64>(diff) * 10000000ULL) / GetTickFrequency();
if (one_hundred_nanos_diff == 0)
return;
LARGE_INTEGER fti;
fti.QuadPart = -static_cast<s64>(one_hundred_nanos_diff);
if (SetWaitableTimer(hTimer, &fti, 0, nullptr, nullptr, FALSE))
{
WaitForSingleObject(hTimer, INFINITE);
return;
}
}

View File

@@ -0,0 +1,289 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/Threading.h"
#include "common/Assertions.h"
#include "common/RedtapeWindows.h"
#include <memory>
#include <mmsystem.h>
#include <process.h>
#include <timeapi.h>
__fi void Threading::Timeslice()
{
::Sleep(0);
}
// For use in spin/wait loops, Acts as a hint to Intel CPUs and should, in theory
// improve performance and reduce cpu power consumption.
__fi void Threading::SpinWait()
{
#ifdef _M_X86
_mm_pause();
#else
YieldProcessor();
#endif
}
__fi void Threading::EnableHiresScheduler()
{
// This improves accuracy of Sleep() by some amount, and only adds a negligible amount of
// overhead on modern CPUs. Typically desktops are already set pretty low, but laptops in
// particular may have a scheduler Period of 15 or 20ms to extend battery life.
// (note: this same trick is used by most multimedia software and games)
timeBeginPeriod(1);
}
__fi void Threading::DisableHiresScheduler()
{
timeEndPeriod(1);
}
Threading::ThreadHandle::ThreadHandle() = default;
Threading::ThreadHandle::ThreadHandle(const ThreadHandle& handle)
{
if (handle.m_native_handle)
{
HANDLE new_handle;
if (DuplicateHandle(GetCurrentProcess(), (HANDLE)handle.m_native_handle,
GetCurrentProcess(), &new_handle, THREAD_QUERY_INFORMATION | THREAD_SET_LIMITED_INFORMATION, FALSE, 0))
{
m_native_handle = (void*)new_handle;
}
}
}
Threading::ThreadHandle::ThreadHandle(ThreadHandle&& handle)
: m_native_handle(handle.m_native_handle)
{
handle.m_native_handle = nullptr;
}
Threading::ThreadHandle::~ThreadHandle()
{
if (m_native_handle)
CloseHandle(m_native_handle);
}
Threading::ThreadHandle Threading::ThreadHandle::GetForCallingThread()
{
ThreadHandle ret;
ret.m_native_handle = (void*)OpenThread(THREAD_QUERY_INFORMATION | THREAD_SET_LIMITED_INFORMATION, FALSE, GetCurrentThreadId());
return ret;
}
Threading::ThreadHandle& Threading::ThreadHandle::operator=(ThreadHandle&& handle)
{
if (m_native_handle)
CloseHandle((HANDLE)m_native_handle);
m_native_handle = handle.m_native_handle;
handle.m_native_handle = nullptr;
return *this;
}
Threading::ThreadHandle& Threading::ThreadHandle::operator=(const ThreadHandle& handle)
{
if (m_native_handle)
{
CloseHandle((HANDLE)m_native_handle);
m_native_handle = nullptr;
}
HANDLE new_handle;
if (DuplicateHandle(GetCurrentProcess(), (HANDLE)handle.m_native_handle,
GetCurrentProcess(), &new_handle, THREAD_QUERY_INFORMATION | THREAD_SET_LIMITED_INFORMATION, FALSE, 0))
{
m_native_handle = (void*)new_handle;
}
return *this;
}
u64 Threading::ThreadHandle::GetCPUTime() const
{
#ifndef _M_ARM64
u64 ret = 0;
if (m_native_handle)
QueryThreadCycleTime((HANDLE)m_native_handle, &ret);
return ret;
#else
FILETIME user, kernel, unused;
if (!GetThreadTimes((HANDLE)m_native_handle, &unused, &unused, &kernel, &user))
return 0;
const u64 user_time = (static_cast<u64>(user.dwHighDateTime) << 32) | static_cast<u64>(user.dwLowDateTime);
const u64 kernel_time = (static_cast<u64>(kernel.dwHighDateTime) << 32) | static_cast<u64>(kernel.dwLowDateTime);
return user_time + kernel_time;
#endif
}
bool Threading::ThreadHandle::SetAffinity(u64 processor_mask) const
{
if (processor_mask == 0)
processor_mask = ~processor_mask;
return (SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)processor_mask) != 0 || GetLastError() != ERROR_SUCCESS);
}
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;
}
unsigned Threading::Thread::ThreadProc(void* param)
{
std::unique_ptr<EntryPoint> entry(static_cast<EntryPoint*>(param));
(*entry.get())();
return 0;
}
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)));
unsigned thread_id;
m_native_handle = reinterpret_cast<void*>(_beginthreadex(nullptr, m_stack_size, ThreadProc, func_clone.get(), 0, &thread_id));
if (!m_native_handle)
return false;
// thread started, it'll release the memory
func_clone.release();
return true;
}
void Threading::Thread::Detach()
{
pxAssertRel(m_native_handle, "Can't detach without a thread");
CloseHandle((HANDLE)m_native_handle);
m_native_handle = nullptr;
}
void Threading::Thread::Join()
{
pxAssertRel(m_native_handle, "Can't join without a thread");
const DWORD res = WaitForSingleObject((HANDLE)m_native_handle, INFINITE);
if (res != WAIT_OBJECT_0)
pxFailRel("WaitForSingleObject() for thread join failed");
CloseHandle((HANDLE)m_native_handle);
m_native_handle = nullptr;
}
Threading::ThreadHandle& Threading::Thread::operator=(Thread&& thread)
{
ThreadHandle::operator=(thread);
m_stack_size = thread.m_stack_size;
thread.m_stack_size = 0;
return *this;
}
u64 Threading::GetThreadCpuTime()
{
#ifndef _M_ARM64
u64 ret = 0;
QueryThreadCycleTime(GetCurrentThread(), &ret);
return ret;
#else
FILETIME user, kernel, unused;
if (!GetThreadTimes(GetCurrentThread(), &unused, &unused, &kernel, &user))
return 0;
const u64 user_time = (static_cast<u64>(user.dwHighDateTime) << 32) | static_cast<u64>(user.dwLowDateTime);
const u64 kernel_time = (static_cast<u64>(kernel.dwHighDateTime) << 32) | static_cast<u64>(kernel.dwLowDateTime);
return user_time + kernel_time;
#endif
}
u64 Threading::GetThreadTicksPerSecond()
{
#ifndef _M_ARM64
// On x86, despite what the MS documentation says, this basically appears to be rdtsc.
// So, the frequency is our base clock speed (and stable regardless of power management).
static u64 frequency = 0;
if (frequency == 0) [[unlikely]]
{
HKEY key;
LSTATUS res =
RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", 0, KEY_READ, &key);
if (res == ERROR_SUCCESS)
{
DWORD mhz;
DWORD size = sizeof(mhz);
res = RegQueryValueExW(key, L"~MHz", nullptr, nullptr, reinterpret_cast<LPBYTE>(&mhz), &size);
if (res == ERROR_SUCCESS)
frequency = static_cast<u64>(mhz) * static_cast<u64>(1000000);
RegCloseKey(key);
}
}
return frequency;
#else
return 10000000;
#endif
}
void Threading::SetNameOfCurrentThread(const char* name)
{
// This feature needs Windows headers and MSVC's SEH support:
#if defined(_WIN32) && defined(_MSC_VER)
// This code sample was borrowed form some obscure MSDN article.
// In a rare bout of sanity, it's an actual Microsoft-published hack
// that actually works!
static const int MS_VC_EXCEPTION = 0x406D1388;
#pragma pack(push, 8)
struct THREADNAME_INFO
{
DWORD dwType; // Must be 0x1000.
LPCSTR szName; // Pointer to name (in user addr space).
DWORD dwThreadID; // Thread ID (-1=caller thread).
DWORD dwFlags; // Reserved for future use, must be zero.
};
#pragma pack(pop)
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = name;
info.dwThreadID = GetCurrentThreadId();
info.dwFlags = 0;
__try
{
RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
}
#endif
}

40
common/WrappedMemCopy.h Normal file
View File

@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/Pcsx2Defs.h"
[[maybe_unused]]
__ri static void MemCopy_WrappedDest(const u128* src, u128* destBase, uint& destStart, uint destSize, uint len)
{
uint endpos = destStart + len;
if (endpos < destSize)
{
memcpy(&destBase[destStart], src, len * 16);
destStart += len;
}
else
{
uint firstcopylen = destSize - destStart;
memcpy(&destBase[destStart], src, firstcopylen * 16);
destStart = endpos % destSize;
memcpy(destBase, src + firstcopylen, destStart * 16);
}
}
[[maybe_unused]]
__ri static void MemCopy_WrappedSrc(const u128* srcBase, uint& srcStart, uint srcSize, u128* dest, uint len)
{
uint endpos = srcStart + len;
if (endpos < srcSize)
{
memcpy(dest, &srcBase[srcStart], len * 16);
srcStart += len;
}
else
{
uint firstcopylen = srcSize - srcStart;
memcpy(dest, &srcBase[srcStart], firstcopylen * 16);
srcStart = endpos % srcSize;
memcpy(dest + firstcopylen, srcBase, srcStart * 16);
}
}

142
common/ZipHelpers.h Normal file
View File

@@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "zip.h"
#include "Console.h"
static inline std::unique_ptr<zip_t, void (*)(zip_t*)> zip_open_managed(const char* filename, int flags, zip_error_t* ze)
{
zip_source_t* zs = zip_source_file_create(filename, 0, 0, ze);
zip_t* zip = nullptr;
if (zs && !(zip = zip_open_from_source(zs, flags, ze)))
{
// have to clean up source
zip_source_free(zs);
}
return std::unique_ptr<zip_t, void (*)(zip_t*)>(zip, [](zip_t* zf) {
if (!zf)
return;
int err = zip_close(zf);
if (err != 0)
{
Console.Error("Failed to close zip file: %d", err);
zip_discard(zf);
}
});
}
static inline std::unique_ptr<zip_t, void (*)(zip_t*)> zip_open_buffer_managed(const void* buffer, size_t size, int flags, int freep, zip_error_t* ze)
{
zip_source_t* zs = zip_source_buffer_create(buffer, size, freep, ze);
zip_t* zip = nullptr;
if (zs && !(zip = zip_open_from_source(zs, flags, ze)))
{
// have to clean up source
zip_source_free(zs);
}
return std::unique_ptr<zip_t, void (*)(zip_t*)>(zip, [](zip_t* zf) {
if (!zf)
return;
int err = zip_close(zf);
if (err != 0)
{
Console.Error("Failed to close zip file: %d", err);
zip_discard(zf);
}
});
}
static inline std::unique_ptr<zip_file_t, int (*)(zip_file_t*)> zip_fopen_managed(zip_t* zip, const char* filename, zip_flags_t flags)
{
return std::unique_ptr<zip_file_t, int (*)(zip_file_t*)>(zip_fopen(zip, filename, flags), zip_fclose);
}
static inline std::unique_ptr<zip_file_t, int (*)(zip_file_t*)> zip_fopen_index_managed(zip_t* zip, zip_uint64_t index, zip_flags_t flags)
{
return std::unique_ptr<zip_file_t, int (*)(zip_file_t*)>(zip_fopen_index(zip, index, flags), zip_fclose);
}
template<typename T>
static inline std::optional<T> ReadFileInZipToContainer(zip_t* zip, const char* name)
{
std::optional<T> ret;
const zip_int64_t file_index = zip_name_locate(zip, name, ZIP_FL_NOCASE);
if (file_index >= 0)
{
zip_stat_t zst;
if (zip_stat_index(zip, file_index, ZIP_FL_NOCASE, &zst) == 0)
{
zip_file_t* zf = zip_fopen_index(zip, file_index, ZIP_FL_NOCASE);
if (zf)
{
ret = T();
ret->resize(static_cast<size_t>(zst.size));
if (zip_fread(zf, ret->data(), ret->size()) != static_cast<zip_int64_t>(ret->size()))
{
ret.reset();
}
}
}
}
return ret;
}
template <typename T>
static inline std::optional<T> ReadFileInZipToContainer(zip_file_t* file, u32 chunk_size = 4096)
{
std::optional<T> ret = T();
for (;;)
{
const size_t pos = ret->size();
ret->resize(pos + chunk_size);
const s64 read = zip_fread(file, ret->data() + pos, chunk_size);
if (read < 0)
{
// read error
ret.reset();
break;
}
// if less than chunk size, we're EOF
if (read != static_cast<s64>(chunk_size))
{
ret->resize(pos + static_cast<size_t>(read));
break;
}
}
return ret;
}
static inline std::optional<std::string> ReadFileInZipToString(zip_t* zip, const char* name)
{
return ReadFileInZipToContainer<std::string>(zip, name);
}
static inline std::optional<std::string> ReadFileInZipToString(zip_file_t* file, u32 chunk_size = 4096)
{
return ReadFileInZipToContainer<std::string>(file, chunk_size);
}
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_t* zip, const char* name)
{
return ReadFileInZipToContainer<std::vector<u8>>(zip, name);
}
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_file_t* file, u32 chunk_size = 4096)
{
return ReadFileInZipToContainer<std::vector<u8>>(file, chunk_size);
}

210
common/boost_spsc_queue.hpp Normal file
View File

@@ -0,0 +1,210 @@
// This version is a stripped down version of boost/lockfree/spsc_queue.hpp boost_spsc_queue.hpp
// Rational
// * Performance is better on linux than the standard std::queue
// * Performance in the same on windows
// => 100-200MB of dependency feel rather unfriendly
// Potential optimization
// * plug condition variable into the queue directly to avoid redundant m_count
// * Restore boost optimization
// => unlikely or replace it with a % (if size is 2^n)
// lock-free single-producer/single-consumer ringbuffer
// this algorithm is implemented in various projects (linux kernel)
//
// Copyright (C) 2009-2013 Tim Blechmann
//
// Distributed under the Boost Software License, Version 1.0. (See
// accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
// Boost Software License - Version 1.0 - August 17th, 2003
//
// Permission is hereby granted, free of charge, to any person or organization
// obtaining a copy of the software and accompanying documentation covered by
// this license (the "Software") to use, reproduce, display, distribute,
// execute, and transmit the Software, and to prepare derivative works of the
// Software, and to permit third-parties to whom the Software is furnished to
// do so, all subject to the following:
//
// The copyright notices in the Software and this entire statement, including
// the above license grant, this restriction and the following disclaimer,
// must be included in all copies of the Software, in whole or in part, and
// all derivative works of the Software, unless such copies or derivative
// works are solely in the form of machine-executable object code generated by
// a source language processor.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#include <atomic>
#include "AlignedMalloc.h"
#include "Pcsx2Defs.h"
template <typename T, size_t max_size>
class ringbuffer_base
{
static const int padding_size = __cachelinesize - sizeof(size_t);
std::atomic<size_t> write_index_;
char padding1[padding_size]; /* force read_index and write_index to different cache lines */
std::atomic<size_t> read_index_;
char padding2[padding_size]; /* force read_index and pending_pop_read_index to different cache lines */
size_t pending_pop_read_index;
T *buffer;
ringbuffer_base(ringbuffer_base const &) = delete;
ringbuffer_base(ringbuffer_base &&) = delete;
const ringbuffer_base& operator=( const ringbuffer_base& ) = delete;
public:
ringbuffer_base(void):
write_index_(0), read_index_(0), pending_pop_read_index(0)
{
// Use dynamically allocation here with no T object dependency
// Otherwise the ringbuffer_base destructor will call the destructor
// of T which crash if T is a (invalid) shared_ptr.
//
// Note another solution will be to create a char buffer as union of T
buffer = (T*)_aligned_malloc(sizeof(T)*max_size, 32);
}
~ringbuffer_base(void) {
// destroy all remaining items
T out;
while (pop(out)) {};
_aligned_free(buffer);
}
static size_t next_index(size_t arg)
{
size_t ret = arg + 1;
#if 0
// Initial boost code
while (unlikely(ret >= max_size))
ret -= max_size;
#else
ret %= max_size;
#endif
return ret;
}
bool push(T const & t)
{
const size_t write_index = write_index_.load(std::memory_order_relaxed); // only written from push thread
const size_t next = next_index(write_index);
if (next == read_index_.load(std::memory_order_acquire))
return false; /* ringbuffer is full */
new (buffer + write_index) T(t); // copy-construct
write_index_.store(next, std::memory_order_release);
return true;
}
bool pop (T & ret)
{
const size_t write_index = write_index_.load(std::memory_order_acquire);
const size_t read_index = read_index_.load(std::memory_order_relaxed); // only written from pop thread
if (empty(write_index, read_index))
return false;
ret = buffer[read_index];
buffer[read_index].~T();
size_t next = next_index(read_index);
read_index_.store(next, std::memory_order_release);
return true;
}
T& front()
{
pending_pop_read_index = read_index_.load(std::memory_order_relaxed); // only written from pop thread
return buffer[pending_pop_read_index];
}
void pop()
{
buffer[pending_pop_read_index].~T();
size_t next = next_index(pending_pop_read_index);
read_index_.store(next, std::memory_order_release);
}
template <typename Functor>
bool consume_one(Functor & f)
{
const size_t write_index = write_index_.load(std::memory_order_acquire);
const size_t read_index = read_index_.load(std::memory_order_relaxed); // only written from pop thread
if (empty(write_index, read_index))
return false;
f(buffer[read_index]);
buffer[read_index].~T();
size_t next = next_index(read_index);
read_index_.store(next, std::memory_order_release);
return true;
}
public:
/** reset the ringbuffer
*
* \note Not thread-safe
* */
void reset(void)
{
write_index_.store(0, std::memory_order_relaxed);
read_index_.store(0, std::memory_order_release);
}
/** Check if the ringbuffer is empty
*
* \return true, if the ringbuffer is empty, false otherwise
* \note Due to the concurrent nature of the ringbuffer the result may be inaccurate.
* */
bool empty(void)
{
return empty(write_index_.load(std::memory_order_relaxed), read_index_.load(std::memory_order_relaxed));
}
/**
* \return true, if implementation is lock-free.
*
* */
bool is_lock_free(void) const
{
return write_index_.is_lock_free() && read_index_.is_lock_free();
}
size_t size() const
{
const size_t write_index = write_index_.load(std::memory_order_relaxed);
const size_t read_index = read_index_.load(std::memory_order_relaxed);
if (read_index > write_index) {
return (write_index + max_size) - read_index;
} else {
return write_index - read_index;
}
}
private:
bool empty(size_t write_index, size_t read_index)
{
return write_index == read_index;
}
};

194
common/common.vcxproj Normal file
View File

@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(SolutionDir)common\vsprops\BaseProjectConfig.props" />
<Import Project="$(SolutionDir)common\vsprops\WinSDK.props" />
<PropertyGroup Label="Globals">
<ProjectGuid>{4639972E-424E-4E13-8B07-CA403C481346}</ProjectGuid>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset Condition="!$(Configuration.Contains(Clang))">$(DefaultPlatformToolset)</PlatformToolset>
<PlatformToolset Condition="$(Configuration.Contains(Clang))">ClangCL</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<WholeProgramOptimization Condition="$(Configuration.Contains(Release))">true</WholeProgramOptimization>
<UseDebugLibraries Condition="$(Configuration.Contains(Debug))">true</UseDebugLibraries>
<UseDebugLibraries Condition="!$(Configuration.Contains(Debug))">false</UseDebugLibraries>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="vsprops\common.props" />
<Import Condition="$(Configuration.Contains(Debug))" Project="vsprops\CodeGen_Debug.props" />
<Import Condition="$(Configuration.Contains(Devel))" Project="vsprops\CodeGen_Devel.props" />
<Import Condition="$(Configuration.Contains(Release))" Project="vsprops\CodeGen_Release.props" />
<Import Condition="!$(Configuration.Contains(Release))" Project="vsprops\IncrementalLinking.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\fast_float\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\fmt\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\jpgd</AdditionalIncludeDirectories>
<PrecompiledHeader>Use</PrecompiledHeader>
<ForcedIncludeFiles>PrecompiledHeader.h</ForcedIncludeFiles>
<PrecompiledHeaderFile>PrecompiledHeader.h</PrecompiledHeaderFile>
<ObjectFileName>$(IntDir)%(RelativeDir)</ObjectFileName>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AlignedMalloc.cpp" />
<ClCompile Include="Assertions.cpp" />
<ClCompile Include="Console.cpp" />
<ClCompile Include="CrashHandler.cpp" />
<ClCompile Include="DynamicLibrary.cpp" />
<ClCompile Include="Error.cpp" />
<ClCompile Include="FastJmp.cpp">
<ExcludedFromBuild Condition="'$(Platform)'!='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="FileSystem.cpp" />
<ClCompile Include="Image.cpp" />
<ClCompile Include="HTTPDownloader.cpp" />
<ClCompile Include="HTTPDownloaderCurl.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="HTTPDownloaderWinHTTP.cpp" />
<ClCompile Include="MD5Digest.cpp" />
<ClCompile Include="MemorySettingsInterface.cpp" />
<ClCompile Include="ProgressCallback.cpp" />
<ClCompile Include="ReadbackSpinManager.cpp" />
<ClCompile Include="SmallString.cpp" />
<ClCompile Include="StackWalker.cpp" />
<ClCompile Include="StringUtil.cpp" />
<ClCompile Include="SettingsWrapper.cpp" />
<ClCompile Include="TextureDecompress.cpp" />
<ClCompile Include="Timer.cpp" />
<ClCompile Include="WAVWriter.cpp" />
<ClCompile Include="WindowInfo.cpp" />
<ClCompile Include="Perf.cpp" />
<ClCompile Include="PrecompiledHeader.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="Windows\WinHostSys.cpp" />
<ClCompile Include="Windows\WinMisc.cpp" />
<ClCompile Include="Windows\WinThreads.cpp" />
<ClCompile Include="HostSys.cpp" />
<ClCompile Include="Semaphore.cpp" />
<ClCompile Include="emitter\avx.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\bmi.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\fpu.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\groups.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\jmp.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\legacy.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\legacy_sse.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\movs.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\simd.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="emitter\x86emitter.cpp">
<ExcludedFromBuild Condition="'$(Platform)'=='ARM64'">true</ExcludedFromBuild>
</ClCompile>
</ItemGroup>
<ItemGroup>
<MASM Include="FastJmp.asm">
<ExcludedFromBuild Condition="'$(Platform)'!='x64'">true</ExcludedFromBuild>
</MASM>
</ItemGroup>
<ItemGroup>
<ClInclude Include="BitUtils.h" />
<ClInclude Include="AlignedMalloc.h" />
<ClInclude Include="ByteSwap.h" />
<ClInclude Include="CrashHandler.h" />
<ClInclude Include="DynamicLibrary.h" />
<ClInclude Include="Easing.h" />
<ClInclude Include="boost_spsc_queue.hpp" />
<ClInclude Include="Error.h" />
<ClInclude Include="FastJmp.h" />
<ClInclude Include="FileSystem.h" />
<ClInclude Include="FPControl.h" />
<ClInclude Include="HashCombine.h" />
<ClInclude Include="HeapArray.h" />
<ClInclude Include="HeterogeneousContainers.h" />
<ClInclude Include="Image.h" />
<ClInclude Include="SingleRegisterTypes.h" />
<ClInclude Include="VectorIntrin.h" />
<ClInclude Include="LRUCache.h" />
<ClInclude Include="HTTPDownloader.h" />
<ClInclude Include="HTTPDownloaderCurl.h">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="HTTPDownloaderWinHTTP.h" />
<ClInclude Include="MD5Digest.h" />
<ClInclude Include="MemorySettingsInterface.h" />
<ClInclude Include="ProgressCallback.h" />
<ClInclude Include="ScopedGuard.h" />
<ClInclude Include="SmallString.h" />
<ClInclude Include="StackWalker.h" />
<ClInclude Include="StringUtil.h" />
<ClInclude Include="SettingsInterface.h" />
<ClInclude Include="SettingsWrapper.h" />
<ClInclude Include="Assertions.h" />
<ClInclude Include="Console.h" />
<ClInclude Include="HostSys.h" />
<ClInclude Include="Path.h" />
<ClInclude Include="PrecompiledHeader.h" />
<ClInclude Include="ReadbackSpinManager.h" />
<ClInclude Include="RedtapeWindows.h" />
<ClInclude Include="TextureDecompress.h" />
<ClInclude Include="Timer.h" />
<ClInclude Include="WAVWriter.h" />
<ClInclude Include="WindowInfo.h" />
<ClInclude Include="Threading.h" />
<ClInclude Include="emitter\implement\avx.h" />
<ClInclude Include="emitter\implement\bmi.h" />
<ClInclude Include="emitter\instructions.h" />
<ClInclude Include="emitter\internal.h" />
<ClInclude Include="emitter\legacy_instructions.h" />
<ClInclude Include="emitter\legacy_internal.h" />
<ClInclude Include="emitter\legacy_types.h" />
<ClInclude Include="emitter\x86emitter.h" />
<ClInclude Include="emitter\x86types.h" />
<ClInclude Include="emitter\implement\dwshift.h" />
<ClInclude Include="emitter\implement\group1.h" />
<ClInclude Include="emitter\implement\group2.h" />
<ClInclude Include="emitter\implement\group3.h" />
<ClInclude Include="emitter\implement\helpers.h" />
<ClInclude Include="emitter\implement\incdec.h" />
<ClInclude Include="emitter\implement\jmpcall.h" />
<ClInclude Include="emitter\implement\movs.h" />
<ClInclude Include="emitter\implement\test.h" />
<ClInclude Include="emitter\implement\xchg.h" />
<ClInclude Include="emitter\implement\simd_arithmetic.h" />
<ClInclude Include="emitter\implement\simd_comparisons.h" />
<ClInclude Include="emitter\implement\simd_helpers.h" />
<ClInclude Include="emitter\implement\simd_moremovs.h" />
<ClInclude Include="emitter\implement\simd_shufflepack.h" />
<ClInclude Include="WrappedMemCopy.h" />
<ClInclude Include="ZipHelpers.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
</ImportGroup>
</Project>

View File

@@ -0,0 +1,352 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="AlignedMalloc.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\bmi.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Console.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\fpu.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\groups.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\jmp.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\legacy.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\legacy_sse.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\avx.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\movs.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HostSys.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Perf.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="PrecompiledHeader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ReadbackSpinManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Semaphore.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\simd.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Windows\WinHostSys.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Windows\WinMisc.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Windows\WinThreads.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="emitter\x86emitter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="FastJmp.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="StringUtil.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SettingsWrapper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowInfo.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="StringUtil.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Timer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ProgressCallback.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MD5Digest.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="StackWalker.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CrashHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Image.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HTTPDownloader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HTTPDownloaderCurl.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HTTPDownloaderWinHTTP.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MemorySettingsInterface.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DynamicLibrary.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WAVWriter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="FileSystem.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Error.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Assertions.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="TextureDecompress.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="SmallString.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="AlignedMalloc.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Assertions.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\bmi.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="boost_spsc_queue.hpp">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Console.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\dwshift.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HostSys.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\group1.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\group2.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\group3.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\helpers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\incdec.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\instructions.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\internal.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\jmpcall.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\legacy_instructions.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\legacy_internal.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\legacy_types.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Path.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="PrecompiledHeader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ReadbackSpinManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="RedtapeWindows.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\simd_arithmetic.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\simd_comparisons.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\simd_helpers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\simd_moremovs.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\simd_shufflepack.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\movs.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\avx.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Threading.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\test.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\x86emitter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\x86types.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="emitter\implement\xchg.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="FastJmp.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="StringUtil.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SettingsInterface.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SettingsWrapper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="BitUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowInfo.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="StringUtil.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ScopedGuard.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Timer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ProgressCallback.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ScopedGuard.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HashCombine.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MD5Digest.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ZipHelpers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CrashHandler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="StackWalker.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LRUCache.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Image.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Easing.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HTTPDownloader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HTTPDownloaderCurl.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HTTPDownloaderWinHTTP.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HeterogeneousContainers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MemorySettingsInterface.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HeapArray.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DynamicLibrary.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WAVWriter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="FileSystem.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ByteSwap.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WrappedMemCopy.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Error.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="TextureDecompress.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SmallString.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="VectorIntrin.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SingleRegisterTypes.h" />
<ClInclude Include="FPControl.h" />
</ItemGroup>
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{2ee889b9-3f2a-4524-a936-962552d20487}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{eef579af-e6a8-4d3b-a88e-c0e4cad9e5d8}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<MASM Include="FastJmp.asm">
<Filter>Source Files</Filter>
</MASM>
</ItemGroup>
</Project>

172
common/emitter/avx.cpp Normal file
View File

@@ -0,0 +1,172 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/emitter/internal.h"
// warning: suggest braces around initialization of subobject [-Wmissing-braces]
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-braces"
#endif
namespace x86Emitter
{
const xImplAVX_Move xVMOVAPS = {0x00, 0x28, 0x29};
const xImplAVX_Move xVMOVUPS = {0x00, 0x10, 0x11};
const xImplAVX_ArithFloat xVADD = {
{0x00, 0x58}, // VADDPS
{0x66, 0x58}, // VADDPD
{0xF3, 0x58}, // VADDSS
{0xF2, 0x58}, // VADDSD
};
const xImplAVX_ArithFloat xVSUB = {
{0x00, 0x5C}, // VSUBPS
{0x66, 0x5C}, // VSUBPD
{0xF3, 0x5C}, // VSUBSS
{0xF2, 0x5C}, // VSUBSD
};
const xImplAVX_ArithFloat xVMUL = {
{0x00, 0x59}, // VMULPS
{0x66, 0x59}, // VMULPD
{0xF3, 0x59}, // VMULSS
{0xF2, 0x59}, // VMULSD
};
const xImplAVX_ArithFloat xVDIV = {
{0x00, 0x5E}, // VDIVPS
{0x66, 0x5E}, // VDIVPD
{0xF3, 0x5E}, // VDIVSS
{0xF2, 0x5E}, // VDIVSD
};
const xImplAVX_CmpFloat xVCMP = {
{SSE2_Equal},
{SSE2_Less},
{SSE2_LessOrEqual},
{SSE2_Unordered},
{SSE2_NotEqual},
{SSE2_NotLess},
{SSE2_NotLessOrEqual},
{SSE2_Ordered},
};
const xImplAVX_ThreeArgYMM xVPAND = {0x66, 0xDB};
const xImplAVX_ThreeArgYMM xVPANDN = {0x66, 0xDF};
const xImplAVX_ThreeArgYMM xVPOR = {0x66, 0xEB};
const xImplAVX_ThreeArgYMM xVPXOR = {0x66, 0xEF};
const xImplAVX_CmpInt xVPCMP = {
{0x66, 0x74}, // VPCMPEQB
{0x66, 0x75}, // VPCMPEQW
{0x66, 0x76}, // VPCMPEQD
{0x66, 0x64}, // VPCMPGTB
{0x66, 0x65}, // VPCMPGTW
{0x66, 0x66}, // VPCMPGTD
};
void xVPMOVMSKB(const xRegister32& to, const xRegisterSSE& from)
{
xOpWriteC5(0x66, 0xd7, to, xRegister32(), from);
}
void xVMOVMSKPS(const xRegister32& to, const xRegisterSSE& from)
{
xOpWriteC5(0x00, 0x50, to, xRegister32(), from);
}
void xVMOVMSKPD(const xRegister32& to, const xRegisterSSE& from)
{
xOpWriteC5(0x66, 0x50, to, xRegister32(), from);
}
void xVZEROUPPER()
{
// rather than dealing with nonexistant operands..
xWrite8(0xc5);
xWrite8(0xf8);
xWrite8(0x77);
}
void xImplAVX_Move::operator()(const xRegisterSSE& to, const xRegisterSSE& from) const
{
if (to != from)
xOpWriteC5(Prefix, LoadOpcode, to, xRegisterSSE(), from);
}
void xImplAVX_Move::operator()(const xRegisterSSE& to, const xIndirectVoid& from) const
{
xOpWriteC5(Prefix, LoadOpcode, to, xRegisterSSE(), from);
}
void xImplAVX_Move::operator()(const xIndirectVoid& to, const xRegisterSSE& from) const
{
xOpWriteC5(Prefix, StoreOpcode, from, xRegisterSSE(), to);
}
void xImplAVX_ThreeArg::operator()(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const
{
pxAssert(!to.IsWideSIMD() && !from1.IsWideSIMD() && !from2.IsWideSIMD());
xOpWriteC5(Prefix, Opcode, to, from1, from2);
}
void xImplAVX_ThreeArg::operator()(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const
{
pxAssert(!to.IsWideSIMD() && !from1.IsWideSIMD());
xOpWriteC5(Prefix, Opcode, to, from1, from2);
}
void xImplAVX_ThreeArgYMM::operator()(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const
{
xOpWriteC5(Prefix, Opcode, to, from1, from2);
}
void xImplAVX_ThreeArgYMM::operator()(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const
{
xOpWriteC5(Prefix, Opcode, to, from1, from2);
}
void xImplAVX_CmpFloatHelper::PS(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const
{
xOpWriteC5(0x00, 0xC2, to, from1, from2);
xWrite8(static_cast<u8>(CType));
}
void xImplAVX_CmpFloatHelper::PS(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const
{
xOpWriteC5(0x00, 0xC2, to, from1, from2);
xWrite8(static_cast<u8>(CType));
}
void xImplAVX_CmpFloatHelper::PD(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const
{
xOpWriteC5(0x66, 0xC2, to, from1, from2);
xWrite8(static_cast<u8>(CType));
}
void xImplAVX_CmpFloatHelper::PD(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const
{
xOpWriteC5(0x66, 0xC2, to, from1, from2);
xWrite8(static_cast<u8>(CType));
}
void xImplAVX_CmpFloatHelper::SS(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const
{
xOpWriteC5(0xF3, 0xC2, to, from1, from2);
xWrite8(static_cast<u8>(CType));
}
void xImplAVX_CmpFloatHelper::SS(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const
{
xOpWriteC5(0xF3, 0xC2, to, from1, from2);
xWrite8(static_cast<u8>(CType));
}
void xImplAVX_CmpFloatHelper::SD(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const
{
xOpWriteC5(0xF2, 0xC2, to, from1, from2);
xWrite8(static_cast<u8>(CType));
}
void xImplAVX_CmpFloatHelper::SD(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const
{
xOpWriteC5(0xF2, 0xC2, to, from1, from2);
xWrite8(static_cast<u8>(CType));
}
} // namespace x86Emitter

22
common/emitter/bmi.cpp Normal file
View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/emitter/internal.h"
namespace x86Emitter
{
const xImplBMI_RVM xMULX = {0xF2, 0x38, 0xF6};
const xImplBMI_RVM xPDEP = {0xF2, 0x38, 0xF5};
const xImplBMI_RVM xPEXT = {0xF3, 0x38, 0xF5};
const xImplBMI_RVM xANDN_S = {0x00, 0x38, 0xF2};
void xImplBMI_RVM::operator()(const xRegisterInt& to, const xRegisterInt& from1, const xRegisterInt& from2) const
{
xOpWriteC4(Prefix, MbPrefix, Opcode, to, from1, from2);
}
void xImplBMI_RVM::operator()(const xRegisterInt& to, const xRegisterInt& from1, const xIndirectVoid& from2) const
{
xOpWriteC4(Prefix, MbPrefix, Opcode, to, from1, from2);
}
} // namespace x86Emitter

60
common/emitter/fpu.cpp Normal file
View File

@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "common/emitter/legacy_internal.h"
//------------------------------------------------------------------
// FPU instructions
//------------------------------------------------------------------
/* fld m32 to fpu reg stack */
emitterT void FLD32(u32 from)
{
xWrite8(0xD9);
ModRM(0, 0x0, DISP32);
xWrite32(MEMADDR(from, 4));
}
// fld st(i)
emitterT void FLD(int st) { xWrite16(0xc0d9 + (st << 8)); }
emitterT void FLD1() { xWrite16(0xe8d9); }
emitterT void FLDL2E() { xWrite16(0xead9); }
/* fstp m32 from fpu reg stack */
emitterT void FSTP32(u32 to)
{
xWrite8(0xD9);
ModRM(0, 0x3, DISP32);
xWrite32(MEMADDR(to, 4));
}
// fstp st(i)
emitterT void FSTP(int st) { xWrite16(0xd8dd + (st << 8)); }
emitterT void FRNDINT() { xWrite16(0xfcd9); }
emitterT void FXCH(int st) { xWrite16(0xc8d9 + (st << 8)); }
emitterT void F2XM1() { xWrite16(0xf0d9); }
emitterT void FSCALE() { xWrite16(0xfdd9); }
emitterT void FPATAN(void) { xWrite16(0xf3d9); }
emitterT void FSIN(void) { xWrite16(0xfed9); }
/* fadd ST(0) to fpu reg stack ST(src) */
emitterT void FADD320toR(x86IntRegType src)
{
xWrite8(0xDC);
xWrite8(0xC0 + src);
}
/* fsub ST(src) to fpu reg stack ST(0) */
emitterT void FSUB32Rto0(x86IntRegType src)
{
xWrite8(0xD8);
xWrite8(0xE0 + src);
}
/* fmul m32 to fpu reg stack */
emitterT void FMUL32(u32 from)
{
xWrite8(0xD8);
ModRM(0, 0x1, DISP32);
xWrite32(MEMADDR(from, 4));
}

259
common/emitter/groups.cpp Normal file
View File

@@ -0,0 +1,259 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
/*
* ix86 core v0.9.1
*
* Original Authors (v0.6.2 and prior):
* linuzappz <linuzappz@pcsx.net>
* alexey silinov
* goldfinger
* zerofrog(@gmail.com)
*
* Authors of v0.9.1:
* Jake.Stine(@gmail.com)
* cottonvibes(@gmail.com)
* sudonim(1@gmail.com)
*/
#include "common/emitter/internal.h"
#include "common/emitter/implement/helpers.h"
namespace x86Emitter
{
// =====================================================================================================
// Group 1 Instructions - ADD, SUB, ADC, etc.
// =====================================================================================================
// Note on "[Indirect],Imm" forms : use int as the source operand since it's "reasonably inert" from a
// compiler perspective. (using uint tends to make the compiler try and fail to match signed immediates
// with one of the other overloads).
static void _g1_IndirectImm(G1Type InstType, const xIndirect64orLess& sibdest, int imm)
{
if (sibdest.Is8BitOp())
{
xOpWrite(sibdest.GetPrefix16(), 0x80, InstType, sibdest, 1);
xWrite<s8>(imm);
}
else
{
u8 opcode = is_s8(imm) ? 0x83 : 0x81;
xOpWrite(sibdest.GetPrefix16(), opcode, InstType, sibdest, is_s8(imm) ? 1 : sibdest.GetImmSize());
if (is_s8(imm))
xWrite<s8>(imm);
else
sibdest.xWriteImm(imm);
}
}
void _g1_EmitOp(G1Type InstType, const xRegisterInt& to, const xRegisterInt& from)
{
pxAssert(to.GetOperandSize() == from.GetOperandSize());
u8 opcode = (to.Is8BitOp() ? 0 : 1) | (InstType << 3);
xOpWrite(to.GetPrefix16(), opcode, from, to);
}
static void _g1_EmitOp(G1Type InstType, const xIndirectVoid& sibdest, const xRegisterInt& from)
{
u8 opcode = (from.Is8BitOp() ? 0 : 1) | (InstType << 3);
xOpWrite(from.GetPrefix16(), opcode, from, sibdest);
}
static void _g1_EmitOp(G1Type InstType, const xRegisterInt& to, const xIndirectVoid& sibsrc)
{
u8 opcode = (to.Is8BitOp() ? 2 : 3) | (InstType << 3);
xOpWrite(to.GetPrefix16(), opcode, to, sibsrc);
}
static void _g1_EmitOp(G1Type InstType, const xRegisterInt& to, int imm)
{
if (!to.Is8BitOp() && is_s8(imm))
{
xOpWrite(to.GetPrefix16(), 0x83, InstType, to);
xWrite<s8>(imm);
}
else
{
if (to.IsAccumulator())
{
u8 opcode = (to.Is8BitOp() ? 4 : 5) | (InstType << 3);
xOpAccWrite(to.GetPrefix16(), opcode, InstType, to);
}
else
{
u8 opcode = to.Is8BitOp() ? 0x80 : 0x81;
xOpWrite(to.GetPrefix16(), opcode, InstType, to);
}
to.xWriteImm(imm);
}
}
#define ImplementGroup1(g1type, insttype) \
void g1type::operator()(const xRegisterInt& to, const xRegisterInt& from) const { _g1_EmitOp(insttype, to, from); } \
void g1type::operator()(const xIndirectVoid& to, const xRegisterInt& from) const { _g1_EmitOp(insttype, to, from); } \
void g1type::operator()(const xRegisterInt& to, const xIndirectVoid& from) const { _g1_EmitOp(insttype, to, from); } \
void g1type::operator()(const xRegisterInt& to, int imm) const { _g1_EmitOp(insttype, to, imm); } \
void g1type::operator()(const xIndirect64orLess& sibdest, int imm) const { _g1_IndirectImm(insttype, sibdest, imm); }
ImplementGroup1(xImpl_Group1, InstType)
ImplementGroup1(xImpl_G1Logic, InstType)
ImplementGroup1(xImpl_G1Arith, InstType)
ImplementGroup1(xImpl_G1Compare, G1Type_CMP)
const xImpl_G1Logic xAND = {G1Type_AND, {0x00, 0x54}, {0x66, 0x54}};
const xImpl_G1Logic xOR = {G1Type_OR, {0x00, 0x56}, {0x66, 0x56}};
const xImpl_G1Logic xXOR = {G1Type_XOR, {0x00, 0x57}, {0x66, 0x57}};
const xImpl_G1Arith xADD = {G1Type_ADD, {0x00, 0x58}, {0x66, 0x58}, {0xf3, 0x58}, {0xf2, 0x58}};
const xImpl_G1Arith xSUB = {G1Type_SUB, {0x00, 0x5c}, {0x66, 0x5c}, {0xf3, 0x5c}, {0xf2, 0x5c}};
const xImpl_G1Compare xCMP = {{0x00, 0xc2}, {0x66, 0xc2}, {0xf3, 0xc2}, {0xf2, 0xc2}};
const xImpl_Group1 xADC = {G1Type_ADC};
const xImpl_Group1 xSBB = {G1Type_SBB};
// =====================================================================================================
// Group 2 Instructions - SHR, SHL, etc.
// =====================================================================================================
void xImpl_Group2::operator()(const xRegisterInt& to, const xRegisterCL& /* from */) const
{
xOpWrite(to.GetPrefix16(), to.Is8BitOp() ? 0xd2 : 0xd3, InstType, to);
}
void xImpl_Group2::operator()(const xRegisterInt& to, u8 imm) const
{
if (imm == 0)
return;
if (imm == 1)
{
// special encoding of 1's
xOpWrite(to.GetPrefix16(), to.Is8BitOp() ? 0xd0 : 0xd1, InstType, to);
}
else
{
xOpWrite(to.GetPrefix16(), to.Is8BitOp() ? 0xc0 : 0xc1, InstType, to);
xWrite8(imm);
}
}
void xImpl_Group2::operator()(const xIndirect64orLess& sibdest, const xRegisterCL& /* from */) const
{
xOpWrite(sibdest.GetPrefix16(), sibdest.Is8BitOp() ? 0xd2 : 0xd3, InstType, sibdest);
}
void xImpl_Group2::operator()(const xIndirect64orLess& sibdest, u8 imm) const
{
if (imm == 0)
return;
if (imm == 1)
{
// special encoding of 1's
xOpWrite(sibdest.GetPrefix16(), sibdest.Is8BitOp() ? 0xd0 : 0xd1, InstType, sibdest);
}
else
{
xOpWrite(sibdest.GetPrefix16(), sibdest.Is8BitOp() ? 0xc0 : 0xc1, InstType, sibdest, 1);
xWrite8(imm);
}
}
const xImpl_Group2 xROL = {G2Type_ROL};
const xImpl_Group2 xROR = {G2Type_ROR};
const xImpl_Group2 xRCL = {G2Type_RCL};
const xImpl_Group2 xRCR = {G2Type_RCR};
const xImpl_Group2 xSHL = {G2Type_SHL};
const xImpl_Group2 xSHR = {G2Type_SHR};
const xImpl_Group2 xSAR = {G2Type_SAR};
// =====================================================================================================
// Group 3 Instructions - NOT, NEG, MUL, DIV
// =====================================================================================================
static void _g3_EmitOp(G3Type InstType, const xRegisterInt& from)
{
xOpWrite(from.GetPrefix16(), from.Is8BitOp() ? 0xf6 : 0xf7, InstType, from);
}
static void _g3_EmitOp(G3Type InstType, const xIndirect64orLess& from)
{
xOpWrite(from.GetPrefix16(), from.Is8BitOp() ? 0xf6 : 0xf7, InstType, from);
}
void xImpl_Group3::operator()(const xRegisterInt& from) const { _g3_EmitOp(InstType, from); }
void xImpl_Group3::operator()(const xIndirect64orLess& from) const { _g3_EmitOp(InstType, from); }
void xImpl_iDiv::operator()(const xRegisterInt& from) const { _g3_EmitOp(G3Type_iDIV, from); }
void xImpl_iDiv::operator()(const xIndirect64orLess& from) const { _g3_EmitOp(G3Type_iDIV, from); }
template <typename SrcType>
static void _imul_ImmStyle(const xRegisterInt& param1, const SrcType& param2, int imm)
{
pxAssert(param1.GetOperandSize() == param2.GetOperandSize());
xOpWrite0F(param1.GetPrefix16(), is_s8(imm) ? 0x6b : 0x69, param1, param2, is_s8(imm) ? 1 : param1.GetImmSize());
if (is_s8(imm))
xWrite8((u8)imm);
else
param1.xWriteImm(imm);
}
void xImpl_iMul::operator()(const xRegisterInt& from) const { _g3_EmitOp(G3Type_iMUL, from); }
void xImpl_iMul::operator()(const xIndirect64orLess& from) const { _g3_EmitOp(G3Type_iMUL, from); }
void xImpl_iMul::operator()(const xRegister32& to, const xRegister32& from) const { xOpWrite0F(0xaf, to, from); }
void xImpl_iMul::operator()(const xRegister32& to, const xIndirectVoid& src) const { xOpWrite0F(0xaf, to, src); }
void xImpl_iMul::operator()(const xRegister16& to, const xRegister16& from) const { xOpWrite0F(0x66, 0xaf, to, from); }
void xImpl_iMul::operator()(const xRegister16& to, const xIndirectVoid& src) const { xOpWrite0F(0x66, 0xaf, to, src); }
void xImpl_iMul::operator()(const xRegister32& to, const xRegister32& from, s32 imm) const { _imul_ImmStyle(to, from, imm); }
void xImpl_iMul::operator()(const xRegister32& to, const xIndirectVoid& from, s32 imm) const { _imul_ImmStyle(to, from, imm); }
void xImpl_iMul::operator()(const xRegister16& to, const xRegister16& from, s16 imm) const { _imul_ImmStyle(to, from, imm); }
void xImpl_iMul::operator()(const xRegister16& to, const xIndirectVoid& from, s16 imm) const { _imul_ImmStyle(to, from, imm); }
const xImpl_Group3 xNOT = {G3Type_NOT};
const xImpl_Group3 xNEG = {G3Type_NEG};
const xImpl_Group3 xUMUL = {G3Type_MUL};
const xImpl_Group3 xUDIV = {G3Type_DIV};
const xImpl_iDiv xDIV = {{0x00, 0x5e}, {0x66, 0x5e}, {0xf3, 0x5e}, {0xf2, 0x5e}};
const xImpl_iMul xMUL = {{0x00, 0x59}, {0x66, 0x59}, {0xf3, 0x59}, {0xf2, 0x59}};
// =====================================================================================================
// Group 8 Instructions
// =====================================================================================================
void xImpl_Group8::operator()(const xRegister16or32or64& bitbase, const xRegister16or32or64& bitoffset) const
{
pxAssert(bitbase->GetOperandSize() == bitoffset->GetOperandSize());
xOpWrite0F(bitbase->GetPrefix16(), 0xa3 | (InstType << 3), bitbase, bitoffset);
}
void xImpl_Group8::operator()(const xIndirect64& bitbase, u8 bitoffset) const { xOpWrite0F(0xba, InstType, bitbase, bitoffset); }
void xImpl_Group8::operator()(const xIndirect32& bitbase, u8 bitoffset) const { xOpWrite0F(0xba, InstType, bitbase, bitoffset); }
void xImpl_Group8::operator()(const xIndirect16& bitbase, u8 bitoffset) const { xOpWrite0F(0x66, 0xba, InstType, bitbase, bitoffset); }
void xImpl_Group8::operator()(const xRegister16or32or64& bitbase, u8 bitoffset) const
{
xOpWrite0F(bitbase->GetPrefix16(), 0xba, InstType, bitbase, bitoffset);
}
void xImpl_Group8::operator()(const xIndirectVoid& bitbase, const xRegister16or32or64& bitoffset) const
{
xOpWrite0F(bitoffset->GetPrefix16(), 0xa3 | (InstType << 3), bitoffset, bitbase);
}
const xImpl_Group8 xBT = {G8Type_BT};
const xImpl_Group8 xBTR = {G8Type_BTR};
const xImpl_Group8 xBTS = {G8Type_BTS};
const xImpl_Group8 xBTC = {G8Type_BTC};
} // End namespace x86Emitter

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
namespace x86Emitter
{
struct xImplAVX_Move
{
u8 Prefix;
u8 LoadOpcode;
u8 StoreOpcode;
void operator()(const xRegisterSSE& to, const xRegisterSSE& from) const;
void operator()(const xRegisterSSE& to, const xIndirectVoid& from) const;
void operator()(const xIndirectVoid& to, const xRegisterSSE& from) const;
};
struct xImplAVX_ThreeArg
{
u8 Prefix;
u8 Opcode;
void operator()(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const;
void operator()(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const;
};
struct xImplAVX_ThreeArgYMM : xImplAVX_ThreeArg
{
void operator()(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const;
void operator()(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const;
};
struct xImplAVX_ArithFloat
{
xImplAVX_ThreeArgYMM PS;
xImplAVX_ThreeArgYMM PD;
xImplAVX_ThreeArg SS;
xImplAVX_ThreeArg SD;
};
struct xImplAVX_CmpFloatHelper
{
SSE2_ComparisonType CType;
void PS(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const;
void PS(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const;
void PD(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const;
void PD(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const;
void SS(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const;
void SS(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const;
void SD(const xRegisterSSE& to, const xRegisterSSE& from1, const xRegisterSSE& from2) const;
void SD(const xRegisterSSE& to, const xRegisterSSE& from1, const xIndirectVoid& from2) const;
};
struct xImplAVX_CmpFloat
{
xImplAVX_CmpFloatHelper EQ;
xImplAVX_CmpFloatHelper LT;
xImplAVX_CmpFloatHelper LE;
xImplAVX_CmpFloatHelper UO;
xImplAVX_CmpFloatHelper NE;
xImplAVX_CmpFloatHelper GE;
xImplAVX_CmpFloatHelper GT;
xImplAVX_CmpFloatHelper OR;
};
struct xImplAVX_CmpInt
{
// Compare packed bytes for equality.
// If a data element in dest is equal to the corresponding date element src, the
// corresponding data element in dest is set to all 1s; otherwise, it is set to all 0s.
const xImplAVX_ThreeArgYMM EQB;
// Compare packed words for equality.
// If a data element in dest is equal to the corresponding date element src, the
// corresponding data element in dest is set to all 1s; otherwise, it is set to all 0s.
const xImplAVX_ThreeArgYMM EQW;
// Compare packed doublewords [32-bits] for equality.
// If a data element in dest is equal to the corresponding date element src, the
// corresponding data element in dest is set to all 1s; otherwise, it is set to all 0s.
const xImplAVX_ThreeArgYMM EQD;
// Compare packed signed bytes for greater than.
// If a data element in dest is greater than the corresponding date element src, the
// corresponding data element in dest is set to all 1s; otherwise, it is set to all 0s.
const xImplAVX_ThreeArgYMM GTB;
// Compare packed signed words for greater than.
// If a data element in dest is greater than the corresponding date element src, the
// corresponding data element in dest is set to all 1s; otherwise, it is set to all 0s.
const xImplAVX_ThreeArgYMM GTW;
// Compare packed signed doublewords [32-bits] for greater than.
// If a data element in dest is greater than the corresponding date element src, the
// corresponding data element in dest is set to all 1s; otherwise, it is set to all 0s.
const xImplAVX_ThreeArgYMM GTD;
};
} // namespace x86Emitter

View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
// Implement BMI1/BMI2 instruction set
namespace x86Emitter
{
struct xImplBMI_RVM
{
u8 Prefix;
u8 MbPrefix;
u8 Opcode;
// RVM
// MULX Unsigned multiply without affecting flags, and arbitrary destination registers
// PDEP Parallel bits deposit
// PEXT Parallel bits extract
// ANDN Logical and not ~x & y
void operator()(const xRegisterInt& to, const xRegisterInt& from1, const xRegisterInt& from2) const;
void operator()(const xRegisterInt& to, const xRegisterInt& from1, const xIndirectVoid& from2) const;
#if 0
// RMV
// BEXTR Bit field extract (with register) (src >> start) & ((1 << len)-1)[9]
// BZHI Zero high bits starting with specified bit position
// SARX Shift arithmetic right without affecting flags
// SHRX Shift logical right without affecting flags
// SHLX Shift logical left without affecting flags
// FIXME: WARNING same as above but V and M are inverted
//void operator()( const xRegisterInt& to, const xRegisterInt& from1, const xRegisterInt& from2) const;
//void operator()( const xRegisterInt& to, const xIndirectVoid& from1, const xRegisterInt& from2) const;
// VM
// BLSI Extract lowest set isolated bit x & -x
// BLSMSK Get mask up to lowest set bit x ^ (x - 1)
// BLSR Reset lowest set bit x & (x - 1)
void operator()( const xRegisterInt& to, const xRegisterInt& from) const;
void operator()( const xRegisterInt& to, const xIndirectVoid& from) const;
// RMI
//RORX Rotate right logical without affecting flags
void operator()( const xRegisterInt& to, const xRegisterInt& from, u8 imm) const;
void operator()( const xRegisterInt& to, const xIndirectVoid& from, u8 imm) const;
#endif
};
} // namespace x86Emitter

View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
namespace x86Emitter
{
// Implementations here cover SHLD and SHRD.
// --------------------------------------------------------------------------------------
// xImpl_DowrdShift
// --------------------------------------------------------------------------------------
// I use explicit method declarations here instead of templates, in order to provide
// *only* 32 and 16 bit register operand forms (8 bit registers are not valid in SHLD/SHRD).
//
// Optimization Note: Imm shifts by 0 are ignore (no code generated). This is a safe optimization
// because shifts by 0 do *not* affect flags status (intel docs cited).
//
struct xImpl_DwordShift
{
u16 OpcodeBase;
void operator()(const xRegister16or32or64& to, const xRegister16or32or64& from, const xRegisterCL& clreg) const;
void operator()(const xRegister16or32or64& to, const xRegister16or32or64& from, u8 shiftcnt) const;
void operator()(const xIndirectVoid& dest, const xRegister16or32or64& from, const xRegisterCL& clreg) const;
void operator()(const xIndirectVoid& dest, const xRegister16or32or64& from, u8 shiftcnt) const;
};
} // End namespace x86Emitter

View File

@@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
namespace x86Emitter
{
enum G1Type
{
G1Type_ADD = 0,
G1Type_OR,
G1Type_ADC,
G1Type_SBB,
G1Type_AND,
G1Type_SUB,
G1Type_XOR,
G1Type_CMP
};
extern void _g1_EmitOp(G1Type InstType, const xRegisterInt& to, const xRegisterInt& from);
// --------------------------------------------------------------------------------------
// xImpl_Group1
// --------------------------------------------------------------------------------------
struct xImpl_Group1
{
G1Type InstType;
void operator()(const xRegisterInt& to, const xRegisterInt& from) const;
void operator()(const xIndirectVoid& to, const xRegisterInt& from) const;
void operator()(const xRegisterInt& to, const xIndirectVoid& from) const;
void operator()(const xRegisterInt& to, int imm) const;
void operator()(const xIndirect64orLess& to, int imm) const;
#if 0
// ------------------------------------------------------------------------
template< typename T > __noinline void operator()( const ModSibBase& to, const xImmReg<T>& immOrReg ) const
{
_DoI_helpermess( *this, to, immOrReg );
}
template< typename T > __noinline void operator()( const xDirectOrIndirect<T>& to, const xImmReg<T>& immOrReg ) const
{
_DoI_helpermess( *this, to, immOrReg );
}
template< typename T > __noinline void operator()( const xDirectOrIndirect<T>& to, int imm ) const
{
_DoI_helpermess( *this, to, imm );
}
template< typename T > __noinline void operator()( const xDirectOrIndirect<T>& to, const xDirectOrIndirect<T>& from ) const
{
_DoI_helpermess( *this, to, from );
}
// FIXME : Make this struct to 8, 16, and 32 bit registers
template< typename T > __noinline void operator()( const xRegisterBase& to, const xDirectOrIndirect<T>& from ) const
{
_DoI_helpermess( *this, xDirectOrIndirect<T>( to ), from );
}
// FIXME : Make this struct to 8, 16, and 32 bit registers
template< typename T > __noinline void operator()( const xDirectOrIndirect<T>& to, const xRegisterBase& from ) const
{
_DoI_helpermess( *this, to, xDirectOrIndirect<T>( from ) );
}
#endif
};
// ------------------------------------------------------------------------
// This class combines x86 with SSE/SSE2 logic operations (ADD, OR, and NOT).
// Note: ANDN [AndNot] is handled below separately.
//
struct xImpl_G1Logic
{
G1Type InstType;
void operator()(const xRegisterInt& to, const xRegisterInt& from) const;
void operator()(const xIndirectVoid& to, const xRegisterInt& from) const;
void operator()(const xRegisterInt& to, const xIndirectVoid& from) const;
void operator()(const xRegisterInt& to, int imm) const;
void operator()(const xIndirect64orLess& to, int imm) const;
xImplSimd_DestRegSSE PS; // packed single precision
xImplSimd_DestRegSSE PD; // packed double precision
};
// ------------------------------------------------------------------------
// This class combines x86 with SSE/SSE2 arithmetic operations (ADD/SUB).
//
struct xImpl_G1Arith
{
G1Type InstType;
void operator()(const xRegisterInt& to, const xRegisterInt& from) const;
void operator()(const xIndirectVoid& to, const xRegisterInt& from) const;
void operator()(const xRegisterInt& to, const xIndirectVoid& from) const;
void operator()(const xRegisterInt& to, int imm) const;
void operator()(const xIndirect64orLess& to, int imm) const;
xImplSimd_DestRegSSE PS; // packed single precision
xImplSimd_DestRegSSE PD; // packed double precision
xImplSimd_DestRegSSE SS; // scalar single precision
xImplSimd_DestRegSSE SD; // scalar double precision
};
// ------------------------------------------------------------------------
struct xImpl_G1Compare
{
void operator()(const xRegisterInt& to, const xRegisterInt& from) const;
void operator()(const xIndirectVoid& to, const xRegisterInt& from) const;
void operator()(const xRegisterInt& to, const xIndirectVoid& from) const;
void operator()(const xRegisterInt& to, int imm) const;
void operator()(const xIndirect64orLess& to, int imm) const;
xImplSimd_DestSSE_CmpImm PS;
xImplSimd_DestSSE_CmpImm PD;
xImplSimd_DestSSE_CmpImm SS;
xImplSimd_DestSSE_CmpImm SD;
};
} // End namespace x86Emitter

Some files were not shown because too many files have changed in this diff Show More